diff --git a/android/2023-emmsale/app/proguard-rules.pro b/android/2023-emmsale/app/proguard-rules.pro index 21f0c22b6..e406c8ab8 100644 --- a/android/2023-emmsale/app/proguard-rules.pro +++ b/android/2023-emmsale/app/proguard-rules.pro @@ -9,4 +9,4 @@ -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation # ApiResponse 클래스 축소 및 난독화 해제하여 CallAdapter에서 retrofit2.Call를 반환하는 CallAdapter 만들 수 있도록 변경 --keep class com.emmsale.data.common.retrofit.callAdapter.ApiResponse +-keepnames class com.emmsale.data.common.retrofit.callAdapter.ApiResponse diff --git a/android/2023-emmsale/app/src/main/AndroidManifest.xml b/android/2023-emmsale/app/src/main/AndroidManifest.xml index 49605754d..5151dc3a1 100644 --- a/android/2023-emmsale/app/src/main/AndroidManifest.xml +++ b/android/2023-emmsale/app/src/main/AndroidManifest.xml @@ -40,15 +40,17 @@ + - + - - + + - - + diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/BlockedMemberResponse.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/BlockedMemberResponse.kt index b10e0d975..fd635322b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/BlockedMemberResponse.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/BlockedMemberResponse.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class BlockedMemberResponse( @SerialName("blockMemberId") - val id: Long, + val blockedMemberId: Long, @SerialName("id") val blockId: Long, @SerialName("memberName") diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/retrofit/callAdapter/KerdyCall.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/retrofit/callAdapter/KerdyCall.kt index 9fce2413a..fb63ed203 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/retrofit/callAdapter/KerdyCall.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/common/retrofit/callAdapter/KerdyCall.kt @@ -20,7 +20,7 @@ class KerdyCall( val apiResult = when { !result.isSuccessful -> Failure(result.code(), result.errorBody()?.string()) responseType == Unit::class.java -> Success(Unit as T, result.headers()) - result.body() == null -> Unexpected(IllegalStateException(NOT_EXIST_BODY)) + result.body() == null -> Unexpected(Throwable(NOT_EXIST_BODY)) else -> Success(result.body()!!, result.headers()) } callback.onResponse(this@KerdyCall, Response.success(apiResult)) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/BlockedMemberMapper.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/BlockedMemberMapper.kt index a36da0dfa..716650ae4 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/BlockedMemberMapper.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/BlockedMemberMapper.kt @@ -6,7 +6,7 @@ import com.emmsale.data.model.BlockedMember fun List.toData(): List = map(BlockedMemberResponse::toData) fun BlockedMemberResponse.toData(): BlockedMember = BlockedMember( - id = id, + blockedMemberId = blockedMemberId, memberName = memberName, blockId = blockId, profileImageUrl = profileImageUrl, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/NotificationMapper.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/NotificationMapper.kt index ce510951b..db73777e9 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/NotificationMapper.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/NotificationMapper.kt @@ -4,17 +4,21 @@ import com.emmsale.data.apiModel.response.CommentTypeNotificationResponse import com.emmsale.data.apiModel.response.EventTypeNotificationResponse import com.emmsale.data.apiModel.response.NotificationResponse import com.emmsale.data.apiModel.response.NotificationResponse.NotificationType -import com.emmsale.data.model.updatedNotification.ChildCommentNotification -import com.emmsale.data.model.updatedNotification.InterestEventNotification -import com.emmsale.data.model.updatedNotification.UpdatedNotification +import com.emmsale.data.model.Comment +import com.emmsale.data.model.Event +import com.emmsale.data.model.Feed +import com.emmsale.data.model.Member +import com.emmsale.data.model.notification.ChildCommentNotification +import com.emmsale.data.model.notification.InterestEventNotification +import com.emmsale.data.model.notification.Notification import kotlinx.serialization.json.Json import java.time.LocalDateTime import java.time.format.DateTimeFormatter @JvmName("NotificationResponse") -fun List.toData(): List = map { it.toData() } +fun List.toData(): List = map { it.toData() } -fun NotificationResponse.toData(): UpdatedNotification = when (notificationType) { +fun NotificationResponse.toData(): Notification = when (notificationType) { NotificationType.EVENT -> { val eventNotificationInformation = Json.decodeFromString( requireNotNull(extraNotificationInformation) { "이벤트 알림에 추가 정보가 없어요" }, @@ -22,10 +26,12 @@ fun NotificationResponse.toData(): UpdatedNotification = when (notificationType) InterestEventNotification( id = notificationId, receiverId = receiverId, - eventId = redirectId, createdAt = createdAt.toLocalDateTime(), isRead = isRead, - eventTitle = eventNotificationInformation.eventTitle, + event = Event( + id = redirectId, + name = eventNotificationInformation.eventTitle, + ), ) } @@ -40,11 +46,13 @@ fun NotificationResponse.toData(): UpdatedNotification = when (notificationType) receiverId = receiverId, createdAt = createdAt.toLocalDateTime(), isRead = isRead, - parentCommentId = commentNotificationInformation.parentId, - childCommentId = redirectId, - childCommentContent = commentNotificationInformation.content, - feedId = commentNotificationInformation.feedId, - commentProfileImageUrl = commentNotificationInformation.writerProfileImageUrl, + comment = Comment( + id = redirectId, + content = commentNotificationInformation.content, + parentCommentId = commentNotificationInformation.parentId, + feed = Feed(id = commentNotificationInformation.feedId), + writer = Member(profileImageUrl = commentNotificationInformation.writerProfileImageUrl), + ), ) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/BlockedMember.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/BlockedMember.kt index 92bced10f..e3537d904 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/BlockedMember.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/BlockedMember.kt @@ -1,7 +1,7 @@ package com.emmsale.data.model data class BlockedMember( - val id: Long, + val blockedMemberId: Long, val memberName: String, val blockId: Long, val profileImageUrl: String, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Comment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Comment.kt index 56cf16d67..4f1bebacb 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Comment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Comment.kt @@ -12,4 +12,6 @@ data class Comment( val updatedAt: LocalDateTime = LocalDateTime.MAX, val isDeleted: Boolean = false, val childComments: List = emptyList(), -) +) { + val isUpdated: Boolean = createdAt != updatedAt +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Config.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Config.kt index a8b7cce45..d1923e1a5 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Config.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Config.kt @@ -2,7 +2,6 @@ package com.emmsale.data.model data class Config( val isNotificationReceive: Boolean, - val isFollowNotificationReceive: Boolean, val isCommentNotificationReceive: Boolean, val isInterestEventNotificationReceive: Boolean, val isMessageNotificationReceive: Boolean, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Event.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Event.kt index 288cbba09..3daeb18d7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Event.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Event.kt @@ -3,7 +3,7 @@ package com.emmsale.data.model import java.time.LocalDateTime data class Event( - val id: Long, + val id: Long = -1, val name: String = "", val informationUrl: String = "", val organization: String? = null, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt index f0abf3b3b..b2fd31b57 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt @@ -14,4 +14,6 @@ data class Feed( val updatedAt: LocalDateTime = LocalDateTime.MAX, ) { val titleImageUrl = imageUrls.firstOrNull() + + val isUpdated = createdAt != updatedAt } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Recruitment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Recruitment.kt index 4524df079..fe93d464f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Recruitment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Recruitment.kt @@ -3,9 +3,9 @@ package com.emmsale.data.model import java.time.LocalDate data class Recruitment( - val id: Long, - val writer: Member, - val event: Event, - val content: String, - val updatedDate: LocalDate, + val id: Long = -1, + val writer: Member = Member(), + val event: Event = Event(), + val content: String = "", + val updatedDate: LocalDate = LocalDate.MAX, ) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/ChildCommentNotification.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/ChildCommentNotification.kt similarity index 50% rename from android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/ChildCommentNotification.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/ChildCommentNotification.kt index a323bdf40..9d4fc3319 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/ChildCommentNotification.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/ChildCommentNotification.kt @@ -1,5 +1,6 @@ -package com.emmsale.data.model.updatedNotification +package com.emmsale.data.model.notification +import com.emmsale.data.model.Comment import java.time.LocalDateTime class ChildCommentNotification( @@ -7,12 +8,8 @@ class ChildCommentNotification( receiverId: Long, createdAt: LocalDateTime, isRead: Boolean, - val parentCommentId: Long, - val childCommentId: Long, - val childCommentContent: String, - val feedId: Long, - val commentProfileImageUrl: String, -) : UpdatedNotification( + val comment: Comment, +) : Notification( id = id, receiverId = receiverId, createdAt = createdAt, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/InterestEventNotification.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/InterestEventNotification.kt similarity index 65% rename from android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/InterestEventNotification.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/InterestEventNotification.kt index 2f9238992..acd498317 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/InterestEventNotification.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/InterestEventNotification.kt @@ -1,5 +1,6 @@ -package com.emmsale.data.model.updatedNotification +package com.emmsale.data.model.notification +import com.emmsale.data.model.Event import java.time.LocalDateTime class InterestEventNotification( @@ -7,9 +8,8 @@ class InterestEventNotification( receiverId: Long, createdAt: LocalDateTime, isRead: Boolean, - val eventId: Long, - val eventTitle: String, -) : UpdatedNotification( + val event: Event, +) : Notification( id = id, receiverId = receiverId, createdAt = createdAt, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/Notification.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/Notification.kt new file mode 100644 index 000000000..4342215c5 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/notification/Notification.kt @@ -0,0 +1,29 @@ +package com.emmsale.data.model.notification + +import java.time.LocalDateTime + +sealed class Notification( + val id: Long, + val receiverId: Long, + val createdAt: LocalDateTime, + val isRead: Boolean, +) { + + fun read(): Notification = when (this) { + is ChildCommentNotification -> ChildCommentNotification( + id = id, + receiverId = receiverId, + createdAt = createdAt, + isRead = true, + comment = comment, + ) + + is InterestEventNotification -> InterestEventNotification( + id = id, + receiverId = receiverId, + createdAt = createdAt, + isRead = true, + event = event, + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/UpdatedNotification.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/UpdatedNotification.kt deleted file mode 100644 index b30a21adb..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/updatedNotification/UpdatedNotification.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.emmsale.data.model.updatedNotification - -import java.time.LocalDateTime - -abstract class UpdatedNotification( - val id: Long, - val receiverId: Long, - val createdAt: LocalDateTime, - val isRead: Boolean, -) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultConfigRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultConfigRepository.kt index 4fa992a3f..5d6120c39 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultConfigRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultConfigRepository.kt @@ -15,10 +15,6 @@ class DefaultConfigRepository @Inject constructor( KEY_NOTIFICATION_RECEIVE, DEFAULT_VALUE_NOTIFICATION_RECEIVE, ), - isFollowNotificationReceive = preference.getBoolean( - KEY_FOLLOW_NOTIFICATION_RECEIVE, - DEFAULT_VALUE_FOLLOW_NOTIFICATION_RECEIVE, - ), isCommentNotificationReceive = preference.getBoolean( KEY_CHILD_COMMENT_NOTIFICATION_RECEIVE, DEFAULT_VALUE_CHILD_COMMENT_NOTIFICATION_RECEIVE, @@ -41,10 +37,6 @@ class DefaultConfigRepository @Inject constructor( preferenceEditor.putBoolean(KEY_NOTIFICATION_RECEIVE, isReceive).commit() } - override fun saveFollowNotificationReceiveConfig(isReceive: Boolean) { - preferenceEditor.putBoolean(KEY_FOLLOW_NOTIFICATION_RECEIVE, isReceive).commit() - } - override fun saveCommentNotificationReceiveConfig(isReceive: Boolean) { preferenceEditor.putBoolean(KEY_CHILD_COMMENT_NOTIFICATION_RECEIVE, isReceive).commit() } @@ -68,9 +60,6 @@ class DefaultConfigRepository @Inject constructor( private const val KEY_AUTO_LOGIN = "auto_login_key" private const val DEFAULT_VALUE_AUTO_LOGIN = false - private const val KEY_FOLLOW_NOTIFICATION_RECEIVE = "follow_notification_receive_key" - private const val DEFAULT_VALUE_FOLLOW_NOTIFICATION_RECEIVE = true - private const val KEY_CHILD_COMMENT_NOTIFICATION_RECEIVE = "child_comment_notification_receive_key" private const val DEFAULT_VALUE_CHILD_COMMENT_NOTIFICATION_RECEIVE = true diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultEventRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultEventRepository.kt index 3fae05231..477dd7cb7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultEventRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultEventRepository.kt @@ -103,7 +103,7 @@ class DefaultEventRepository @Inject constructor( ) } - override suspend fun deleteScrap(eventId: Long): ApiResponse { + override suspend fun scrapOffEvent(eventId: Long): ApiResponse { return eventService.deleteScrap(eventId) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultNotificationRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultNotificationRepository.kt index ef055336d..35917091c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultNotificationRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultNotificationRepository.kt @@ -1,11 +1,10 @@ package com.emmsale.data.repository.concretes import com.emmsale.data.apiModel.request.NotificationListDeleteRequest -import com.emmsale.data.apiModel.request.RecruitmentNotificationReportCreateRequest import com.emmsale.data.apiModel.response.NotificationResponse import com.emmsale.data.common.retrofit.callAdapter.ApiResponse import com.emmsale.data.mapper.toData -import com.emmsale.data.model.updatedNotification.UpdatedNotification +import com.emmsale.data.model.notification.Notification import com.emmsale.data.repository.interfaces.NotificationRepository import com.emmsale.data.service.NotificationService import com.emmsale.di.modules.other.IoDispatcher @@ -18,45 +17,25 @@ class DefaultNotificationRepository @Inject constructor( private val notificationService: NotificationService, ) : NotificationRepository { - override suspend fun updateNotificationReadStatus( - notificationId: Long, - ): ApiResponse = withContext(dispatcher) { - notificationService.updateRecruitmentNotificationReadStatus(notificationId) - } - - override suspend fun getUpdatedNotifications( + override suspend fun getNotifications( memberId: Long, - ): ApiResponse> = withContext(dispatcher) { + ): ApiResponse> = withContext(dispatcher) { notificationService .getNotifications(memberId) .map(List::toData) } - override suspend fun updateUpdatedNotificationReadStatus( + override suspend fun readNotification( notificationId: Long, ): ApiResponse = withContext(dispatcher) { - notificationService.updateNotificationReadStatus(notificationId) + notificationService.readNotification(notificationId) } - override suspend fun deleteUpdatedNotifications( + override suspend fun deleteNotifications( notificationIds: List, ): ApiResponse = withContext(dispatcher) { notificationService.deleteNotification( NotificationListDeleteRequest(notificationIds), ) } - - override suspend fun reportRecruitmentNotification( - recruitmentNotificationId: Long, - senderId: Long, - reporterId: Long, - ): ApiResponse = withContext(dispatcher) { - notificationService.reportRecruitmentNotification( - RecruitmentNotificationReportCreateRequest.create( - recruitmentNotificationId = recruitmentNotificationId, - senderId = senderId, - reporterId = reporterId, - ), - ).map { } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/ConfigRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/ConfigRepository.kt index 00a6179e7..f9d499458 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/ConfigRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/ConfigRepository.kt @@ -10,8 +10,6 @@ interface ConfigRepository { fun saveAutoLoginConfig(isAutoLogin: Boolean) - fun saveFollowNotificationReceiveConfig(isReceive: Boolean) - fun saveCommentNotificationReceiveConfig(isReceive: Boolean) fun saveInterestEventNotificationReceiveConfig(isReceive: Boolean) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/EventRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/EventRepository.kt index 4a8bf85d9..bfa89859b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/EventRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/EventRepository.kt @@ -38,7 +38,7 @@ interface EventRepository { suspend fun scrapEvent(eventId: Long): ApiResponse - suspend fun deleteScrap(eventId: Long): ApiResponse + suspend fun scrapOffEvent(eventId: Long): ApiResponse suspend fun isScraped(eventId: Long): ApiResponse } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/NotificationRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/NotificationRepository.kt index 5d0b92829..6176de4d1 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/NotificationRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/NotificationRepository.kt @@ -1,21 +1,13 @@ package com.emmsale.data.repository.interfaces import com.emmsale.data.common.retrofit.callAdapter.ApiResponse -import com.emmsale.data.model.updatedNotification.UpdatedNotification +import com.emmsale.data.model.notification.Notification interface NotificationRepository { - suspend fun updateNotificationReadStatus(notificationId: Long): ApiResponse + suspend fun getNotifications(memberId: Long): ApiResponse> - suspend fun getUpdatedNotifications(memberId: Long): ApiResponse> + suspend fun readNotification(notificationId: Long): ApiResponse - suspend fun updateUpdatedNotificationReadStatus(notificationId: Long): ApiResponse - - suspend fun deleteUpdatedNotifications(notificationIds: List): ApiResponse - - suspend fun reportRecruitmentNotification( - recruitmentNotificationId: Long, - senderId: Long, - reporterId: Long, - ): ApiResponse + suspend fun deleteNotifications(notificationIds: List): ApiResponse } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/NotificationService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/NotificationService.kt index 01cdf3ffe..d46846a89 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/NotificationService.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/NotificationService.kt @@ -1,36 +1,16 @@ package com.emmsale.data.service import com.emmsale.data.apiModel.request.NotificationListDeleteRequest -import com.emmsale.data.apiModel.request.RecruitmentNotificationReportCreateRequest -import com.emmsale.data.apiModel.request.RecruitmentNotificationStatusUpdateRequest -import com.emmsale.data.apiModel.response.NotificationReportResponse import com.emmsale.data.apiModel.response.NotificationResponse -import com.emmsale.data.apiModel.response.RecruitmentNotificationResponse import com.emmsale.data.common.retrofit.callAdapter.ApiResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.HTTP import retrofit2.http.PATCH -import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query interface NotificationService { - @GET("/request-notifications") - suspend fun getRecruitmentNotifications( - @Query("member-id") memberId: Long, - ): ApiResponse> - - @PATCH("/request-notifications/{request-notification-id}/status") - suspend fun updateRecruitmentStatus( - @Path("request-notification-id") notificationId: Long, - @Body newStatus: RecruitmentNotificationStatusUpdateRequest, - ): ApiResponse - - @PATCH("/request-notifications/{request-notification-id}/read") - suspend fun updateRecruitmentNotificationReadStatus( - @Path("request-notification-id") notificationId: Long, - ): ApiResponse @GET("/notifications") suspend fun getNotifications( @@ -38,7 +18,7 @@ interface NotificationService { ): ApiResponse> @PATCH("/notifications/{notification-id}/read") - suspend fun updateNotificationReadStatus( + suspend fun readNotification( @Path("notification-id") notificationId: Long, ): ApiResponse @@ -46,9 +26,4 @@ interface NotificationService { suspend fun deleteNotification( @Body notificationIds: NotificationListDeleteRequest, ): ApiResponse - - @POST("/reports") - suspend fun reportRecruitmentNotification( - @Body recruitmentNotificationReportCreateRequest: RecruitmentNotificationReportCreateRequest, - ): ApiResponse } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseActivity.kt new file mode 100644 index 000000000..4d7ccbe9f --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseActivity.kt @@ -0,0 +1,21 @@ +package com.emmsale.presentation.base + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding + +abstract class BaseActivity( + @LayoutRes private val layoutResId: Int, +) : AppCompatActivity() { + + protected val binding: V by lazy { + DataBindingUtil.inflate(layoutInflater, layoutResId, null, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.lifecycleOwner = this + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseFragment.kt index ca8c096e4..8d009904c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/BaseFragment.kt @@ -4,12 +4,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.LayoutRes import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment -abstract class BaseFragment : Fragment() { - abstract val layoutResId: Int +abstract class BaseFragment( + @LayoutRes private val layoutResId: Int, +) : Fragment() { private var _binding: V? = null protected val binding get() = _binding!! diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkActivity.kt new file mode 100644 index 000000000..a7c43e342 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkActivity.kt @@ -0,0 +1,55 @@ +package com.emmsale.presentation.base + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.databinding.ViewDataBinding +import com.emmsale.R +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.views.InfoDialog + +abstract class NetworkActivity( + @LayoutRes layoutResId: Int, +) : BaseActivity(layoutResId) { + + protected abstract val viewModel: NetworkViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + observeCommonUiEvent() + } + + private fun observeCommonUiEvent() { + viewModel.commonUiEvent.observe(this) { handleCommonUiEvent(it) } + } + + private fun handleCommonUiEvent(event: CommonUiEvent) { + when (event) { + CommonUiEvent.RequestFailByNetworkError -> binding.root.showSnackBar(getString(R.string.all_network_check_message)) + is CommonUiEvent.Unexpected -> showUnexpectedErrorOccurredDialog() + is CommonUiEvent.FetchFail -> showIllegalAccessDialog() + } + } + + private fun showUnexpectedErrorOccurredDialog() { + InfoDialog( + context = this, + title = getString(R.string.all_fetch_error_dialog_title), + message = getString(R.string.all_unexpected_error_occurred_dialog_message), + buttonLabel = getString(R.string.all_okay), + onButtonClick = { finish() }, + cancelable = false, + ).show() + } + + private fun showIllegalAccessDialog() { + InfoDialog( + context = this, + title = getString(R.string.all_fetch_error_dialog_title), + message = getString(R.string.all_fetch_error_dialog_message), + buttonLabel = getString(R.string.all_okay), + onButtonClick = { finish() }, + cancelable = false, + ).show() + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkFragment.kt new file mode 100644 index 000000000..c9b2d28e4 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkFragment.kt @@ -0,0 +1,55 @@ +package com.emmsale.presentation.base + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.databinding.ViewDataBinding +import com.emmsale.R +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.views.InfoDialog + +abstract class NetworkFragment( + @LayoutRes layoutResId: Int, +) : BaseFragment(layoutResId) { + + abstract val viewModel: NetworkViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + observeCommentUiEvent() + } + + private fun observeCommentUiEvent() { + viewModel.commonUiEvent.observe(this) { handleCommentUiEvent(it) } + } + + private fun handleCommentUiEvent(event: CommonUiEvent) { + when (event) { + CommonUiEvent.RequestFailByNetworkError -> binding.root.showSnackBar(getString(R.string.all_network_check_message)) + is CommonUiEvent.Unexpected -> showUnexpectedErrorOccurredDialog() + is CommonUiEvent.FetchFail -> showIllegalAccessDialog() + } + } + + private fun showUnexpectedErrorOccurredDialog() { + InfoDialog( + context = requireContext(), + title = getString(R.string.all_fetch_error_dialog_title), + message = getString(R.string.all_unexpected_error_occurred_dialog_message), + buttonLabel = getString(R.string.all_okay), + onButtonClick = { activity?.finish() }, + cancelable = false, + ).show() + } + + private fun showIllegalAccessDialog() { + InfoDialog( + context = requireContext(), + title = getString(R.string.all_fetch_error_dialog_title), + message = getString(R.string.all_fetch_error_dialog_message), + buttonLabel = getString(R.string.all_okay), + onButtonClick = { activity?.finish() }, + cancelable = false, + ).show() + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkViewModel.kt new file mode 100644 index 000000000..c2c4f3d1c --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/NetworkViewModel.kt @@ -0,0 +1,96 @@ +package com.emmsale.presentation.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.emmsale.data.common.retrofit.callAdapter.ApiResponse +import com.emmsale.data.common.retrofit.callAdapter.Failure +import com.emmsale.data.common.retrofit.callAdapter.NetworkError +import com.emmsale.data.common.retrofit.callAdapter.Success +import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.ScreenUiState +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +abstract class NetworkViewModel : ViewModel() { + + protected val _screenUiState = NotNullMutableLiveData(ScreenUiState.NONE) + val screenUiState: NotNullLiveData = _screenUiState + + protected val _commonUiEvent = SingleLiveEvent() + val commonUiEvent: LiveData = _commonUiEvent + + protected fun changeToLoadingState() { + _screenUiState.value = ScreenUiState.LOADING + } + + protected fun changeToNetworkErrorState() { + _screenUiState.value = ScreenUiState.NETWORK_ERROR + } + + protected fun dispatchNetworkErrorEvent() { + _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError + } + + protected suspend fun delayLoading(timeMillis: Long = LOADING_DELAY) { + delay(timeMillis) + changeToLoadingState() + } + + protected fun requestNetwork( + request: suspend () -> ApiResponse, + onSuccess: suspend (T) -> Unit, + onFailure: suspend (code: Int, message: String?) -> Unit, + onLoading: suspend () -> Unit, + onNetworkError: suspend () -> Unit, + onStart: suspend () -> Unit = {}, + onFinish: suspend () -> Unit = {}, + ): Job = viewModelScope.launch { + onStart() + val loadingJob = launch { onLoading() } + when (val result = request()) { + is Success -> onSuccess(result.data) + is Failure -> onFailure(result.code, result.message) + NetworkError -> { + onNetworkError() + if (_screenUiState.value == ScreenUiState.NETWORK_ERROR) { + onFinish() + return@launch + } + } + + is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) + } + loadingJob.cancel() + _screenUiState.value = ScreenUiState.NONE + onFinish() + } + + protected fun command( + command: suspend () -> ApiResponse, + onSuccess: suspend (T) -> Unit = {}, + onFailure: suspend (code: Int, message: String?) -> Unit = { _, _ -> }, + onLoading: suspend () -> Unit = { delayLoading() }, + onNetworkError: suspend () -> Unit = { dispatchNetworkErrorEvent() }, + onStart: suspend () -> Unit = {}, + onFinish: suspend () -> Unit = {}, + ): Job = requestNetwork( + request = { command() }, + onSuccess = { onSuccess(it) }, + onFailure = { code, message -> onFailure(code, message) }, + onLoading = { onLoading() }, + onNetworkError = { onNetworkError() }, + onStart = { onStart() }, + onFinish = { onFinish() }, + ) + + companion object { + private const val LOADING_DELAY: Long = 1000 + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/RefreshableViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/RefreshableViewModel.kt new file mode 100644 index 000000000..2bbd44091 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/base/RefreshableViewModel.kt @@ -0,0 +1,72 @@ +package com.emmsale.presentation.base + +import com.emmsale.data.common.retrofit.callAdapter.ApiResponse +import com.emmsale.presentation.common.CommonUiEvent +import kotlinx.coroutines.Job + +abstract class RefreshableViewModel : NetworkViewModel() { + + abstract fun refresh(): Job + + protected fun dispatchFetchFailEvent() { + _commonUiEvent.value = CommonUiEvent.FetchFail + } + + protected fun fetchData( + fetchData: suspend () -> ApiResponse, + onSuccess: suspend (T) -> Unit = {}, + onFailure: suspend (code: Int, message: String?) -> Unit = { _, _ -> dispatchFetchFailEvent() }, + onLoading: suspend () -> Unit = { changeToLoadingState() }, + onNetworkError: suspend () -> Unit = { changeToNetworkErrorState() }, + onStart: suspend () -> Unit = {}, + onFinish: suspend () -> Unit = {}, + ): Job = requestNetwork( + request = { fetchData() }, + onSuccess = { onSuccess(it) }, + onFailure = { code, message -> onFailure(code, message) }, + onLoading = { onLoading() }, + onNetworkError = { onNetworkError() }, + onStart = { onStart() }, + onFinish = { onFinish() }, + ) + + protected fun refreshData( + refresh: suspend () -> ApiResponse, + onSuccess: suspend (T) -> Unit = {}, + onFailure: suspend (code: Int, message: String?) -> Unit = { _, _ -> dispatchFetchFailEvent() }, + onLoading: suspend () -> Unit = {}, + onNetworkError: suspend () -> Unit = {}, + onStart: suspend () -> Unit = {}, + onFinish: suspend () -> Unit = {}, + ): Job = requestNetwork( + request = { refresh() }, + onSuccess = { onSuccess(it) }, + onFailure = { code, message -> onFailure(code, message) }, + onLoading = { onLoading() }, + onNetworkError = { onNetworkError() }, + onStart = { onStart() }, + onFinish = { onFinish() }, + ) + + protected fun commandAndRefresh( + command: suspend () -> ApiResponse, + onSuccess: suspend (T) -> Unit = {}, + onFailure: suspend (code: Int, message: String?) -> Unit = { _, _ -> }, + onLoading: suspend () -> Unit = { delayLoading() }, + onNetworkError: suspend () -> Unit = { dispatchNetworkErrorEvent() }, + onStart: suspend () -> Unit = {}, + onFinish: suspend () -> Unit = {}, + refresh: suspend () -> Job = { this@RefreshableViewModel.refresh() }, + ): Job = requestNetwork( + request = { command() }, + onSuccess = { + refresh().join() + onSuccess(it) + }, + onFailure = { code, message -> onFailure(code, message) }, + onLoading = { onLoading() }, + onNetworkError = { onNetworkError() }, + onStart = { onStart() }, + onFinish = { onFinish() }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt index b2d975e82..cae80bcb6 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/CommonUiEvent.kt @@ -2,5 +2,6 @@ package com.emmsale.presentation.common sealed interface CommonUiEvent { data class Unexpected(val errorMessage: String) : CommonUiEvent + object FetchFail : CommonUiEvent object RequestFailByNetworkError : CommonUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KerdyNotificationChannel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KerdyNotificationChannel.kt index a5354d25c..72cc1a457 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KerdyNotificationChannel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/KerdyNotificationChannel.kt @@ -13,9 +13,9 @@ enum class KerdyNotificationChannel( private val channelImportance: Int = NotificationManager.IMPORTANCE_HIGH, ) { CHILD_COMMENT( - channelId = R.id.id_all_child_comment_notification_channel.toString(), - channelNameResId = R.string.kerdyfirebasemessaging_child_comment_notification_channel_name, - channelDescResId = R.string.kerdyfirebasemessaging_child_comment_notification_channel_description, + channelId = R.id.id_all_comment_notification_channel.toString(), + channelNameResId = R.string.kerdyfirebasemessaging_comment_notification_channel_name, + channelDescResId = R.string.kerdyfirebasemessaging_comment_notification_channel_description, ), INTEREST_EVENT( channelId = R.id.id_all_interest_event_notification_channel.toString(), diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/LocalDateTimeBindingAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/LocalDateTimeBindingAdapter.kt index cfb1202bc..f1206d667 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/LocalDateTimeBindingAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/bindingadapter/LocalDateTimeBindingAdapter.kt @@ -7,6 +7,7 @@ import com.emmsale.R import com.emmsale.presentation.common.extension.format import com.emmsale.presentation.common.extension.toRelativeDateTime import com.emmsale.presentation.common.extension.toRelativeTime +import java.time.LocalDate import java.time.LocalDateTime @BindingAdapter("app:dateText", "app:dateTimeFormatter", requireAll = false) @@ -43,6 +44,14 @@ fun TextView.setDateRange( ) } +@BindingAdapter("app:dateText", "app:dateTimeFormatter") +fun TextView.setDate( + localDate: LocalDate, + dateTimePattern: DateTimePattern, +) { + text = dateTimePattern.format(context, localDate.atStartOfDay()) +} + enum class DateTimePattern { MONTH_DAY_WEEKDAY { override fun format(context: Context, localDateTime: LocalDateTime): String { @@ -69,6 +78,16 @@ enum class DateTimePattern { return localDateTime.format(context, R.string.am_pm_hour_minute) } }, + YEAR_DOT_MONTH_DOT_DAY { + override fun format(context: Context, localDateTime: LocalDateTime): String { + return localDateTime.format(context, R.string.year_month_day) + } + }, + YEAR_DASH_MONTH_DASH_DAY { + override fun format(context: Context, localDateTime: LocalDateTime): String { + return localDateTime.format(context, R.string.year_dash_month_dash_day) + } + }, ; abstract fun format(context: Context, localDateTime: LocalDateTime): String diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt index ed1dd6108..d1f8151a3 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/BasicTextInputWindow.kt @@ -5,13 +5,24 @@ import android.util.AttributeSet import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use -import androidx.databinding.BindingAdapter +import androidx.databinding.BindingMethod +import androidx.databinding.BindingMethods import com.emmsale.R import com.emmsale.databinding.LayoutBasicInputWindowBinding import com.emmsale.presentation.common.extension.dp -import com.emmsale.presentation.common.views.BasicTextInputWindow.OnSubmitListener -import kotlin.properties.Delegates.observable +@BindingMethods( + BindingMethod( + type = BasicTextInputWindow::class, + attribute = "onSubmit", + method = "setOnSubmitListener", + ), + BindingMethod( + type = BasicTextInputWindow::class, + attribute = "isSubmitEnabled", + method = "setIsSubmitEnabled", + ), +) class BasicTextInputWindow @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -21,14 +32,6 @@ class BasicTextInputWindow @JvmOverloads constructor( LayoutBasicInputWindowBinding.inflate(LayoutInflater.from(context), this, false) } - var isSubmitEnabled: Boolean by observable(false) { _, _, newValue -> - binding.isSubmitEnabled = newValue - } - - var onSubmitListener: OnSubmitListener by observable(OnSubmitListener { }) { _, _, newValue -> - binding.onSubmitListener = newValue - } - init { applyStyledAttributes(attrs) setPadding(17.dp, 8.dp, 17.dp, 8.dp) @@ -49,6 +52,14 @@ class BasicTextInputWindow @JvmOverloads constructor( } } + fun setOnSubmitListener(onSubmitListener: OnSubmitListener) { + binding.onSubmitListener = onSubmitListener + } + + fun setIsSubmitEnabled(enabled: Boolean) { + binding.isSubmitEnabled = enabled + } + fun clearText() { binding.etBasicInput.text.clear() } @@ -57,13 +68,3 @@ class BasicTextInputWindow @JvmOverloads constructor( fun onSubmit(text: String) } } - -@BindingAdapter("app:onSubmit") -fun BasicTextInputWindow.setOnSubmitListener(onSubmitListener: OnSubmitListener) { - this.onSubmitListener = onSubmitListener -} - -@BindingAdapter("app:isSubmitEnabled") -fun BasicTextInputWindow.setIsSubmitEnabled(isSubmitEnabled: Boolean) { - this.isSubmitEnabled = isSubmitEnabled -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/VerticalDivider.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/HorizontalDivider.kt similarity index 72% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/VerticalDivider.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/HorizontalDivider.kt index 448a1b0df..0c0aaf3cf 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/VerticalDivider.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/HorizontalDivider.kt @@ -5,11 +5,11 @@ import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import com.emmsale.R -class VerticalDivider( +class HorizontalDivider @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, ) : AppCompatImageView(context, attrs) { init { - setBackgroundResource(R.drawable.bg_all_vertical_divider) + setImageResource(R.drawable.bg_all_horizontal_divider) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt index 841ab7e50..fd7cb1664 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/SubTextInputWindow.kt @@ -6,14 +6,34 @@ import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use import androidx.core.graphics.drawable.toDrawable -import androidx.databinding.BindingAdapter +import androidx.databinding.BindingMethod +import androidx.databinding.BindingMethods import com.emmsale.R import com.emmsale.databinding.LayoutSubTextInputWindowBinding import com.emmsale.presentation.common.extension.dp -import com.emmsale.presentation.common.views.SubTextInputWindow.OnCancelListener -import com.emmsale.presentation.common.views.SubTextInputWindow.OnSubmitListener -import kotlin.properties.Delegates.observable +@BindingMethods( + BindingMethod( + type = SubTextInputWindow::class, + attribute = "text", + method = "setText", + ), + BindingMethod( + type = SubTextInputWindow::class, + attribute = "isSubmitEnabled", + method = "setIsSubmitEnabled", + ), + BindingMethod( + type = SubTextInputWindow::class, + attribute = "onSubmit", + method = "setOnSubmitListener", + ), + BindingMethod( + type = SubTextInputWindow::class, + attribute = "onCancel", + method = "setOnCancelListener", + ), +) class SubTextInputWindow @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -23,22 +43,6 @@ class SubTextInputWindow @JvmOverloads constructor( LayoutSubTextInputWindowBinding.inflate(LayoutInflater.from(context), this, false) } - var text: String by observable("") { _, _, newValue -> - binding.etSubTextInput.setText(newValue) - } - - var isSubmitEnabled: Boolean by observable(false) { _, _, newValue -> - binding.isSubmitEnabled = newValue - } - - var onSubmitListener: OnSubmitListener by observable(OnSubmitListener { }) { _, _, newValue -> - binding.onSubmitListener = newValue - } - - var onCancelListener: OnCancelListener by observable(OnCancelListener { }) { _, _, newValue -> - binding.onCancelListener = newValue - } - init { applyStyledAttributes(attrs) addView(binding.root) @@ -60,6 +64,22 @@ class SubTextInputWindow @JvmOverloads constructor( } } + fun setText(text: String?) { + if (text != null) binding.etSubTextInput.setText(text) + } + + fun setIsSubmitEnabled(enabled: Boolean) { + binding.isSubmitEnabled = enabled + } + + fun setOnSubmitListener(onSubmitListener: OnSubmitListener) { + binding.onSubmitListener = onSubmitListener + } + + fun setOnCancelListener(onCancelListener: OnCancelListener) { + binding.onCancelListener = onCancelListener + } + fun requestFocusOnEditText() { binding.etSubTextInput.requestFocus() } @@ -72,23 +92,3 @@ class SubTextInputWindow @JvmOverloads constructor( fun onCancel() } } - -@BindingAdapter("app:text") -fun SubTextInputWindow.setText(text: String?) { - if (text != null) this.text = text -} - -@BindingAdapter("app:isSubmitEnabled") -fun SubTextInputWindow.setIsSubmitEnabled(isSubmitEnabled: Boolean) { - this.isSubmitEnabled = isSubmitEnabled -} - -@BindingAdapter("app:onSubmit") -fun SubTextInputWindow.setOnSubmitListener(onSubmitListener: OnSubmitListener) { - this.onSubmitListener = onSubmitListener -} - -@BindingAdapter("app:onCancel") -fun SubTextInputWindow.setOnCancelListener(onCancelListener: OnCancelListener) { - this.onCancelListener = onCancelListener -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/bottomMenuDialog/BottomMenuDialog.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/bottomMenuDialog/BottomMenuDialog.kt index 41e60827e..1f87dabc3 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/bottomMenuDialog/BottomMenuDialog.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/common/views/bottomMenuDialog/BottomMenuDialog.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.view.ViewGroup import com.emmsale.R import com.emmsale.databinding.DialogBottomMenuBinding -import com.emmsale.presentation.common.views.VerticalDivider +import com.emmsale.presentation.common.views.HorizontalDivider import com.google.android.material.bottomsheet.BottomSheetDialog class BottomMenuDialog( @@ -19,13 +19,20 @@ class BottomMenuDialog( initDialogWindow() } + private fun initDialogWindow() { + window?.attributes?.let { + it.width = ViewGroup.LayoutParams.MATCH_PARENT + it.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + fun addMenuItemBelow( title: String, menuItemType: MenuItemType = MenuItemType.NORMAL, onClick: () -> Unit, ) { if (binding.llBottommenudialogMenuitems.childCount > 0) { - binding.llBottommenudialogMenuitems.addView(VerticalDivider(context)) + binding.llBottommenudialogMenuitems.addView(HorizontalDivider(context)) } binding.llBottommenudialogMenuitems.addView(createMenuItem(title, menuItemType, onClick)) } @@ -46,11 +53,4 @@ class BottomMenuDialog( dismiss() } } - - private fun initDialogWindow() { - window?.attributes?.let { - it.width = ViewGroup.LayoutParams.MATCH_PARENT - it.height = ViewGroup.LayoutParams.WRAP_CONTENT - } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt index 9854b1f17..b867c9071 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/service/KerdyFirebaseMessagingService.kt @@ -3,10 +3,7 @@ package com.emmsale.presentation.service import android.content.Intent import android.net.Uri import com.emmsale.R -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.repository.interfaces.CommentRepository import com.emmsale.data.repository.interfaces.ConfigRepository import com.emmsale.data.repository.interfaces.TokenRepository @@ -15,8 +12,8 @@ import com.emmsale.presentation.common.extension.isUpdateNeeded import com.emmsale.presentation.common.extension.showNotification import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.extension.topActivityName -import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity import com.emmsale.presentation.ui.eventDetail.EventDetailActivity +import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity import com.emmsale.presentation.ui.messageList.MessageListActivity import com.emmsale.presentation.ui.splash.SplashActivity import com.google.android.play.core.appupdate.AppUpdateManagerFactory @@ -72,30 +69,21 @@ class KerdyFirebaseMessagingService : FirebaseMessagingService() { if (!isNotificationReceive || tokenRepository.getToken() == null) return when (message.data["notificationType"]?.uppercase()) { - CHILD_COMMENT_NOTIFICATION_TYPE -> { + COMMENT_NOTIFICATION_TYPE -> { if (config.isCommentNotificationReceive) { - showChildCommentNotification( - message, - isUpdateNeeded, - ) + showCommentNotification(message, isUpdateNeeded) } } EVENT_NOTIFICATION_TYPE -> { if (config.isInterestEventNotificationReceive) { - showInterestEventNotification( - message, - isUpdateNeeded, - ) + showInterestEventNotification(message, isUpdateNeeded) } } MESSAGE_NOTIFICATION_TYPE -> { if (config.isMessageNotificationReceive) { - showMessageNotification( - message, - isUpdateNeeded, - ) + showMessageNotification(message, isUpdateNeeded) } } } @@ -108,38 +96,37 @@ class KerdyFirebaseMessagingService : FirebaseMessagingService() { startActivity(intent) } - private fun showChildCommentNotification(message: RemoteMessage, isUpdateNeeded: Boolean) { - fun getFeedIdAndParentCommentId(commentId: Long): Pair { + private fun showCommentNotification(message: RemoteMessage, isUpdateNeeded: Boolean) { + fun getFeedId(commentId: Long): Long { return runBlocking { when (val result = commentRepository.getComment(commentId)) { - is Failure, NetworkError -> ERROR_FEED_ID to ERROR_FEED_ID - is Success -> result.data.feed.id to result.data.id - is Unexpected -> throw Throwable(result.error) + is Success -> result.data.feed.id + else -> ERROR_FEED_ID } } } - val childCommentId = message.data["redirectId"]?.toLong() ?: return + val commentId = message.data["redirectId"]?.toLong() ?: return val content = message.data["content"] ?: return val writerName = message.data["writer"] ?: return val writerImageUrl = message.data["writerImageUrl"] ?: return - val (feedId, parentCommentId) = getFeedIdAndParentCommentId(childCommentId) + val feedId = getFeedId(commentId) if (feedId == ERROR_FEED_ID) return val intent = if (isUpdateNeeded) { SplashActivity.getIntent(this) } else { - ChildCommentActivity.getIntent(this, feedId, parentCommentId, childCommentId, false) + FeedDetailActivity.getIntent(this, feedId, commentId) } baseContext.showNotification( - title = getString(R.string.kerdyfirebasemessaging_child_comment_notification_title), - message = getString(R.string.kerdyfirebasemessaging_child_comment_notification_content_format).format( + title = getString(R.string.kerdyfirebasemessaging_comment_notification_title), + message = getString(R.string.kerdyfirebasemessaging_comment_notification_content_format).format( writerName, content, ), - channelId = R.id.id_all_child_comment_notification_channel, + channelId = R.id.id_all_comment_notification_channel, intent = intent, largeIconUrl = writerImageUrl, ) @@ -206,7 +193,7 @@ class KerdyFirebaseMessagingService : FirebaseMessagingService() { companion object { private const val ERROR_FEED_ID = -1L - private const val CHILD_COMMENT_NOTIFICATION_TYPE = "COMMENT" + private const val COMMENT_NOTIFICATION_TYPE = "COMMENT" private const val EVENT_NOTIFICATION_TYPE = "EVENT" private const val MESSAGE_NOTIFICATION_TYPE = "MESSAGE" } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersActivity.kt similarity index 54% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockActivity.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersActivity.kt index 7dcae2e9f..f98bcb3f2 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersActivity.kt @@ -4,9 +4,9 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R -import com.emmsale.databinding.ActivityMemberBlockBinding +import com.emmsale.databinding.ActivityBlockedMembersBinding +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.views.ConfirmDialog import com.emmsale.presentation.ui.blockMemberList.recyclerView.BlockedMemberAdapter @@ -14,73 +14,70 @@ import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMembersUiEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MemberBlockActivity : AppCompatActivity() { - private val binding by lazy { ActivityMemberBlockBinding.inflate(layoutInflater) } - private val viewModel: MemberBlockViewModel by viewModels() +class BlockedMembersActivity : + NetworkActivity(R.layout.activity_blocked_members) { + + override val viewModel: BlockedMembersViewModel by viewModels() private val blockedMemberAdapter: BlockedMemberAdapter by lazy { BlockedMemberAdapter(::showUnblockMemberDialog) } + private fun showUnblockMemberDialog(blockId: Long) { + ConfirmDialog( + context = this, + title = getString(R.string.memberblock_unblock_member_dialog_title), + message = getString(R.string.memberblock_unblock_member_dialog_message), + onPositiveButtonClick = { viewModel.unblockMember(blockId) }, + ).show() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - initView() + + setupDateBinding() + setupToolbar() + setupBlockedMembersRecyclerView() + + observeBlockedMembers() + observeUiEvent() } - private fun initView() { + private fun setupDateBinding() { binding.viewModel = viewModel - binding.lifecycleOwner = this - initToolbarMenuClickListener() - initBlockedMemberRecyclerView() - setupBlockedMembersObserver() - setupUiEvent() } - private fun initToolbarMenuClickListener() { + private fun setupToolbar() { binding.tbMemberBlock.setOnMenuItemClickListener { item -> if (item.itemId == R.id.close) finish() true } } - private fun initBlockedMemberRecyclerView() { + private fun setupBlockedMembersRecyclerView() { binding.rvBlockedMembers.adapter = blockedMemberAdapter } - private fun setupBlockedMembersObserver() { - viewModel.blockedMembers.observe(this) { uiState -> - if (!uiState.isLoading) blockedMemberAdapter.submitList(uiState.blockedMembers) + private fun observeBlockedMembers() { + viewModel.blockedMembers.observe(this) { + blockedMemberAdapter.submitList(it) } } - private fun setupUiEvent() { - viewModel.event.observe(this) { + private fun observeUiEvent() { + viewModel.uiEvent.observe(this) { handleEvent(it) } } - private fun handleEvent(event: BlockedMembersUiEvent?) { - if (event == null) return - when (event) { - BlockedMembersUiEvent.DELETE_ERROR -> showBlockedMemberDeletingErrorMessage() + private fun handleEvent(uiEvent: BlockedMembersUiEvent) { + when (uiEvent) { + BlockedMembersUiEvent.DeleteFail -> binding.root.showSnackBar(R.string.memberblock_unblock_member_failed_message) + BlockedMembersUiEvent.FetchFail -> binding.root.showSnackBar(R.string.memberblock_loading_blocked_members_failed_message) } - viewModel.resetEvent() - } - - private fun showBlockedMemberDeletingErrorMessage() { - binding.root.showSnackBar(R.string.memberblock_unblock_member_failed_message) - } - - private fun showUnblockMemberDialog(blockId: Long) { - ConfirmDialog( - context = this, - title = getString(R.string.memberblock_unblock_member_dialog_title), - message = getString(R.string.memberblock_unblock_member_dialog_message), - onPositiveButtonClick = { viewModel.unblockMember(blockId) }, - ).show() } companion object { fun startActivity(context: Context) { - context.startActivity(Intent(context, MemberBlockActivity::class.java)) + context.startActivity(Intent(context, BlockedMembersActivity::class.java)) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersViewModel.kt new file mode 100644 index 000000000..e09ca9995 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/BlockedMembersViewModel.kt @@ -0,0 +1,47 @@ +package com.emmsale.presentation.ui.blockMemberList + +import androidx.lifecycle.LiveData +import com.emmsale.data.model.BlockedMember +import com.emmsale.data.repository.interfaces.BlockedMemberRepository +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMembersUiEvent +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject + +@HiltViewModel +class BlockedMembersViewModel @Inject constructor( + private val blockedMemberRepository: BlockedMemberRepository, +) : RefreshableViewModel() { + private val _blockedMembers = NotNullMutableLiveData(listOf()) + val blockedMembers: NotNullLiveData> = _blockedMembers + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent + + init { + fetchBlockedMembers() + } + + private fun fetchBlockedMembers(): Job = fetchData( + fetchData = { blockedMemberRepository.getBlockedMembers() }, + onSuccess = { _blockedMembers.value = it }, + onFailure = { _, _ -> _uiEvent.value = BlockedMembersUiEvent.FetchFail }, + ) + + override fun refresh(): Job = refreshData( + refresh = { blockedMemberRepository.getBlockedMembers() }, + onSuccess = { _blockedMembers.value = it }, + ) + + fun unblockMember(blockId: Long): Job = command( + command = { blockedMemberRepository.deleteBlockedMember(blockId) }, + onSuccess = { + _blockedMembers.value = _blockedMembers.value.filter { it.blockId != blockId } + }, + onFailure = { _, _ -> _uiEvent.value = BlockedMembersUiEvent.DeleteFail }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockViewModel.kt deleted file mode 100644 index 6f55d2665..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/MemberBlockViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.emmsale.presentation.ui.blockMemberList - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.repository.interfaces.BlockedMemberRepository -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMembersUiEvent -import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMembersUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class MemberBlockViewModel @Inject constructor( - private val blockedMemberRepository: BlockedMemberRepository, -) : ViewModel(), Refreshable { - private val _blockedMembers = NotNullMutableLiveData(BlockedMembersUiState()) - val blockedMembers: NotNullLiveData = _blockedMembers - - private val _event = MutableLiveData(null) - val event: LiveData = _event - - init { - refresh() - } - - override fun refresh() { - fetchBlockedMembers() - } - - private fun fetchBlockedMembers() { - viewModelScope.launch { - _blockedMembers.value = _blockedMembers.value.copy(isLoading = true) - - when (val result = blockedMemberRepository.getBlockedMembers()) { - is Failure, NetworkError -> _blockedMembers.value = blockedMembers.value.copy( - isLoading = false, - isError = true, - ) - - is Success -> - _blockedMembers.value = BlockedMembersUiState.from(result.data) - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun unblockMember(blockId: Long) { - viewModelScope.launch { - when (val result = blockedMemberRepository.deleteBlockedMember(blockId)) { - is Failure, NetworkError -> _event.value = BlockedMembersUiEvent.DELETE_ERROR - is Success -> - _blockedMembers.value = blockedMembers.value.deleteBlockedMember(blockId) - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun resetEvent() { - _event.value = null - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberAdapter.kt index a6f19af7c..bacaa9b22 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberAdapter.kt @@ -2,11 +2,11 @@ package com.emmsale.presentation.ui.blockMemberList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMemberUiState +import com.emmsale.data.model.BlockedMember class BlockedMemberAdapter( private val onUnblockMemberClick: (memberId: Long) -> Unit, -) : ListAdapter(BlockedMemberDiffUtil) { +) : ListAdapter(BlockedMemberDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockedMemberViewHolder = BlockedMemberViewHolder(parent, onUnblockMemberClick) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberDiffUtil.kt index 4b637553b..636208f81 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberDiffUtil.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberDiffUtil.kt @@ -1,16 +1,16 @@ package com.emmsale.presentation.ui.blockMemberList.recyclerView import androidx.recyclerview.widget.DiffUtil -import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMemberUiState +import com.emmsale.data.model.BlockedMember -object BlockedMemberDiffUtil : DiffUtil.ItemCallback() { +object BlockedMemberDiffUtil : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: BlockedMemberUiState, - newItem: BlockedMemberUiState, - ): Boolean = oldItem.id == newItem.id + oldItem: BlockedMember, + newItem: BlockedMember, + ): Boolean = oldItem.blockId == newItem.blockId override fun areContentsTheSame( - oldItem: BlockedMemberUiState, - newItem: BlockedMemberUiState, + oldItem: BlockedMember, + newItem: BlockedMember, ): Boolean = oldItem == newItem } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberViewHolder.kt index 66cd3c24a..307b3bb1d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/recyclerView/BlockedMemberViewHolder.kt @@ -4,8 +4,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.emmsale.R +import com.emmsale.data.model.BlockedMember import com.emmsale.databinding.ItemBlockMemberBinding -import com.emmsale.presentation.ui.blockMemberList.uiState.BlockedMemberUiState class BlockedMemberViewHolder( parent: ViewGroup, @@ -19,7 +19,7 @@ class BlockedMemberViewHolder( binding.onUnblockMemberClick = onUnblockMemberClick } - fun bind(blockedMember: BlockedMemberUiState) { + fun bind(blockedMember: BlockedMember) { binding.blockedMember = blockedMember } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMemberUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMemberUiState.kt deleted file mode 100644 index 06c4464ab..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMemberUiState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.emmsale.presentation.ui.blockMemberList.uiState - -import com.emmsale.data.model.BlockedMember - -data class BlockedMemberUiState( - val id: Long, - val blockId: Long, - val name: String, - val profileImageUrl: String, -) { - companion object { - fun from(blockedMember: BlockedMember): BlockedMemberUiState = BlockedMemberUiState( - id = blockedMember.id, - blockId = blockedMember.blockId, - name = blockedMember.memberName, - profileImageUrl = blockedMember.profileImageUrl, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiEvent.kt index a78696c72..50ec55a70 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiEvent.kt @@ -1,5 +1,6 @@ package com.emmsale.presentation.ui.blockMemberList.uiState -enum class BlockedMembersUiEvent { - DELETE_ERROR, +sealed interface BlockedMembersUiEvent { + object FetchFail : BlockedMembersUiEvent + object DeleteFail : BlockedMembersUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiState.kt deleted file mode 100644 index 3cc5f9851..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/blockMemberList/uiState/BlockedMembersUiState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.emmsale.presentation.ui.blockMemberList.uiState - -import com.emmsale.data.model.BlockedMember - -data class BlockedMembersUiState( - val blockedMembers: List = emptyList(), - val isLoading: Boolean = false, - val isError: Boolean = false, -) { - fun deleteBlockedMember(blockId: Long): BlockedMembersUiState = - copy(blockedMembers = blockedMembers.filterNot { it.blockId == blockId }) - - companion object { - fun from(blockedMembers: List): BlockedMembersUiState = - BlockedMembersUiState(blockedMembers = blockedMembers.map(BlockedMemberUiState.Companion::from)) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt deleted file mode 100644 index 6ad070d11..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentViewModel.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.map -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.repository.interfaces.CommentRepository -import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.CommonUiEvent -import com.emmsale.presentation.common.ScreenUiState -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.livedata.SingleLiveEvent -import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent -import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import javax.inject.Inject -import kotlin.properties.Delegates.vetoable - -@HiltViewModel -class ChildCommentViewModel @Inject constructor( - stateHandle: SavedStateHandle, - private val tokenRepository: TokenRepository, - private val commentRepository: CommentRepository, -) : ViewModel() { - - var isAlreadyFirstFetched: Boolean by vetoable(false) { _, _, newValue -> - newValue - } - - private val parentCommentId = stateHandle.get(KEY_PARENT_COMMENT_ID)!! - - val feedId = stateHandle.get(KEY_FEED_ID)!! - private val uid: Long by lazy { tokenRepository.getMyUid()!! } - - private val _screenUiState = NotNullMutableLiveData(ScreenUiState.LOADING) - val screenUiState: NotNullLiveData = _screenUiState - - private val _comments = NotNullMutableLiveData(ChildCommentsUiState()) - val comments: NotNullLiveData = _comments - - private val _editingCommentId = MutableLiveData() - val editingCommentId: LiveData = _editingCommentId - - val editingCommentContent: LiveData = _editingCommentId.map { - _comments.value.comments - .find { commentUiState -> commentUiState.comment.id == it } - ?.comment - ?.content - } - - private val _commonUiEvent = SingleLiveEvent() - val commonUiEvent: LiveData = _commonUiEvent - - private val _uiEvent = SingleLiveEvent() - val uiEvent: LiveData = _uiEvent - - init { - fetchComments() - } - - private fun fetchComments(): Job = viewModelScope.launch { - _screenUiState.value = ScreenUiState.LOADING - when (val result = commentRepository.getComment(parentCommentId)) { - is Failure -> _uiEvent.value = ChildCommentsUiEvent.IllegalCommentFetch - NetworkError -> { - _screenUiState.value = ScreenUiState.NETWORK_ERROR - return@launch - } - - is Success -> _comments.value = ChildCommentsUiState.create(uid, result.data) - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - _screenUiState.value = ScreenUiState.NONE - } - - fun postChildComment(content: String): Job = viewModelScope.launch { - val loadingJob = launch { - delay(LOADING_DELAY) - _screenUiState.value = ScreenUiState.LOADING - } - when (val result = commentRepository.saveComment(content, feedId, parentCommentId)) { - is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentPostFail - NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - is Success -> { - refresh().join() - _uiEvent.value = ChildCommentsUiEvent.CommentPostComplete - } - - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - loadingJob.cancel() - _screenUiState.value = ScreenUiState.NONE - } - - fun updateComment(commentId: Long, content: String): Job = viewModelScope.launch { - val loadingJob = launch { - delay(LOADING_DELAY) - _screenUiState.value = ScreenUiState.LOADING - } - when (val result = commentRepository.updateComment(commentId, content)) { - is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentUpdateFail - NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - - is Success -> { - refresh().join() - _editingCommentId.value = null - } - - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - loadingJob.cancel() - _screenUiState.value = ScreenUiState.NONE - } - - fun deleteComment(commentId: Long): Job = viewModelScope.launch { - val loadingJob = launch { - delay(LOADING_DELAY) - _screenUiState.value = ScreenUiState.LOADING - } - when (val result = commentRepository.deleteComment(commentId)) { - is Failure -> _uiEvent.value = ChildCommentsUiEvent.CommentDeleteFail - NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - is Success -> refresh().join() - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - loadingJob.cancel() - _screenUiState.value = ScreenUiState.NONE - } - - fun setEditMode(isEditMode: Boolean, commentId: Long = INVALID_COMMENT_ID) { - _editingCommentId.value = if (isEditMode) commentId else null - } - - fun reportComment(commentId: Long): Job = viewModelScope.launch { - val loadingJob = launch { - delay(LOADING_DELAY) - _screenUiState.value = ScreenUiState.LOADING - } - val authorId = - _comments.value.comments.find { it.comment.id == commentId }!!.comment.writer.id - when (val result = commentRepository.reportComment(commentId, authorId, uid)) { - is Failure -> { - if (result.code == REPORT_DUPLICATE_ERROR_CODE) { - _uiEvent.value = ChildCommentsUiEvent.CommentReportDuplicate - } else { - _uiEvent.value = ChildCommentsUiEvent.CommentReportFail - } - } - - NetworkError -> _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - is Success -> _uiEvent.value = ChildCommentsUiEvent.CommentReportComplete - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - loadingJob.cancel() - _screenUiState.value = ScreenUiState.NONE - } - - fun refresh(): Job = viewModelScope.launch { - when (val result = commentRepository.getComment(parentCommentId)) { - is Failure -> {} - NetworkError -> { - _commonUiEvent.value = CommonUiEvent.RequestFailByNetworkError - return@launch - } - - is Success -> _comments.value = ChildCommentsUiState.create(uid, result.data) - is Unexpected -> - _commonUiEvent.value = CommonUiEvent.Unexpected(result.error.toString()) - } - _screenUiState.value = ScreenUiState.NONE - } - - fun highlight(commentId: Long) { - val comment = _comments.value.comments.find { it.comment.id == commentId } ?: return - if (comment.isHighlight) return - _comments.value = _comments.value.highlight(commentId) - } - - fun unhighlight(commentId: Long) { - val comment = _comments.value.comments.find { it.comment.id == commentId } ?: return - if (!comment.isHighlight) return - _comments.value = _comments.value.unhighlight(commentId) - } - - companion object { - const val KEY_FEED_ID = "KEY_FEED_ID" - const val KEY_PARENT_COMMENT_ID = "KEY_PARENT_COMMENT_ID" - - private const val INVALID_COMMENT_ID: Long = -1 - - private const val REPORT_DUPLICATE_ERROR_CODE = 400 - - private const val LOADING_DELAY: Long = 1000 - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsActivity.kt similarity index 84% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsActivity.kt index 5615c6198..533193ed0 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsActivity.kt @@ -6,34 +6,33 @@ import android.content.Intent import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.emmsale.R +import com.emmsale.data.model.Comment import com.emmsale.databinding.ActivityChildCommentsBinding -import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.hideKeyboard import com.emmsale.presentation.common.extension.showKeyboard import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.childCommentList.ChildCommentViewModel.Companion.KEY_FEED_ID -import com.emmsale.presentation.ui.childCommentList.ChildCommentViewModel.Companion.KEY_PARENT_COMMENT_ID +import com.emmsale.presentation.ui.childCommentList.ChildCommentsViewModel.Companion.KEY_FEED_ID +import com.emmsale.presentation.ui.childCommentList.ChildCommentsViewModel.Companion.KEY_PARENT_COMMENT_ID +import com.emmsale.presentation.ui.childCommentList.recyclerView.CommentsAdapter import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent -import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiState import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity -import com.emmsale.presentation.ui.feedDetail.recyclerView.CommentsAdapter +import com.emmsale.presentation.ui.feedDetail.uiState.CommentsUiState import com.emmsale.presentation.ui.profile.ProfileActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ChildCommentActivity : AppCompatActivity() { - private val binding by lazy { ActivityChildCommentsBinding.inflate(layoutInflater) } +class ChildCommentsActivity : + NetworkActivity(R.layout.activity_child_comments) { - private val viewModel: ChildCommentViewModel by viewModels() + override val viewModel: ChildCommentsViewModel by viewModels() private val commentsAdapter: CommentsAdapter = CommentsAdapter( onCommentClick = { comment -> viewModel.unhighlight(comment.id) }, @@ -51,30 +50,13 @@ class ChildCommentActivity : AppCompatActivity() { intent.getBooleanExtra(KEY_FROM_POST_DETAIL, true) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - setupDataBinding() - setupBackPressedDispatcher() - setupToolbar() - setupChildCommentsRecyclerView() - observeComments() - observeUiEvent() - } - - override fun onRestart() { - super.onRestart() - viewModel.refresh() - } - - private fun showCommentMenuDialog(isWrittenByLoginUser: Boolean, commentId: Long) { + private fun showCommentMenuDialog(isWrittenByLoginUser: Boolean, comment: Comment) { bottomMenuDialog.resetMenu() if (isWrittenByLoginUser) { - bottomMenuDialog.addCommentUpdateButton(commentId) - bottomMenuDialog.addCommentDeleteButton(commentId) + bottomMenuDialog.addCommentUpdateButton(comment.id) + bottomMenuDialog.addCommentDeleteButton(comment.id) } else { - bottomMenuDialog.addCommentReportButton(commentId) + bottomMenuDialog.addCommentReportButton(comment.id) } bottomMenuDialog.show() } @@ -123,9 +105,26 @@ class ChildCommentActivity : AppCompatActivity() { ).show() } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + setupChildCommentsRecyclerView() + + observeComments() + observeUiEvent() + } + + override fun onRestart() { + super.onRestart() + viewModel.refresh() + } + private fun setupDataBinding() { binding.viewModel = viewModel - binding.lifecycleOwner = this binding.onCommentSubmitButtonClick = { viewModel.postChildComment(it) hideKeyboard() @@ -148,7 +147,7 @@ class ChildCommentActivity : AppCompatActivity() { override fun handleOnBackPressed() { if (!fromPostDetail) { FeedDetailActivity.startActivity( - this@ChildCommentActivity, + this@ChildCommentsActivity, viewModel.feedId, ) } @@ -167,24 +166,24 @@ class ChildCommentActivity : AppCompatActivity() { binding.rvChildcommentsChildcomments.apply { adapter = commentsAdapter itemAnimator = null - addItemDecoration(DividerItemDecoration(this@ChildCommentActivity)) + addItemDecoration(DividerItemDecoration(this@ChildCommentsActivity)) } } private fun observeComments() { viewModel.comments.observe(this) { - commentsAdapter.submitList(it.comments) { scrollToIfFirstFetch(it) } + commentsAdapter.submitList(it.commentUiStates) { scrollToIfFirstFetch(it) } } } - private fun scrollToIfFirstFetch(childCommentUiState: ChildCommentsUiState) { + private fun scrollToIfFirstFetch(commentUiState: CommentsUiState) { fun cantScroll(): Boolean = - viewModel.isAlreadyFirstFetched || childCommentUiState.comments.isEmpty() + viewModel.isAlreadyFirstFetched || commentUiState.commentUiStates.isEmpty() if (highlightCommentId == INVALID_COMMENT_ID || cantScroll()) return - val position = viewModel.comments.value.comments - .indexOfFirst { commentUiState -> - commentUiState.comment.id == highlightCommentId + val position = viewModel.comments.value.commentUiStates + .indexOfFirst { + it.comment.id == highlightCommentId } binding.rvChildcommentsChildcomments.scrollToPosition(position) @@ -193,8 +192,7 @@ class ChildCommentActivity : AppCompatActivity() { } private fun observeUiEvent() { - viewModel.uiEvent.observe(this) { handleUiEvent(it) } - viewModel.commonUiEvent.observe(this) { handleBaseUiEvent(it) } + viewModel.uiEvent.observe(this, ::handleUiEvent) } private fun handleUiEvent(event: ChildCommentsUiEvent) { @@ -236,15 +234,8 @@ class ChildCommentActivity : AppCompatActivity() { } } - private fun handleBaseUiEvent(event: CommonUiEvent) { - when (event) { - CommonUiEvent.RequestFailByNetworkError -> binding.root.showSnackBar(getString(R.string.all_network_check_message)) - is CommonUiEvent.Unexpected -> showToast(event.errorMessage) - } - } - private fun smoothScrollToLastPosition() { - binding.rvChildcommentsChildcomments.smoothScrollToPosition(viewModel.comments.value.comments.size) + binding.rvChildcommentsChildcomments.smoothScrollToPosition(viewModel.comments.value.commentUiStates.size) } companion object { @@ -275,7 +266,7 @@ class ChildCommentActivity : AppCompatActivity() { parentCommentId: Long, highlightCommentId: Long = INVALID_COMMENT_ID, fromPostDetail: Boolean = true, - ) = Intent(context, ChildCommentActivity::class.java) + ) = Intent(context, ChildCommentsActivity::class.java) .putExtra(KEY_FEED_ID, feedId) .putExtra(KEY_PARENT_COMMENT_ID, parentCommentId) .putExtra(KEY_HIGHLIGHT_COMMENT_ID, highlightCommentId) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsViewModel.kt new file mode 100644 index 000000000..023431030 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/ChildCommentsViewModel.kt @@ -0,0 +1,129 @@ +package com.emmsale.presentation.ui.childCommentList + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.map +import com.emmsale.data.repository.interfaces.CommentRepository +import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.childCommentList.uiState.ChildCommentsUiEvent +import com.emmsale.presentation.ui.feedDetail.uiState.CommentsUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject +import kotlin.properties.Delegates.vetoable + +@HiltViewModel +class ChildCommentsViewModel @Inject constructor( + stateHandle: SavedStateHandle, + private val tokenRepository: TokenRepository, + private val commentRepository: CommentRepository, +) : RefreshableViewModel() { + + var isAlreadyFirstFetched: Boolean by vetoable(false) { _, _, newValue -> + newValue + } + + private val parentCommentId = stateHandle.get(KEY_PARENT_COMMENT_ID)!! + + val feedId = stateHandle.get(KEY_FEED_ID)!! + private val uid: Long by lazy { tokenRepository.getMyUid()!! } + + private val _comments = NotNullMutableLiveData(CommentsUiState()) + val comments: NotNullLiveData = _comments + + private val _editingCommentId = MutableLiveData() + val editingCommentId: LiveData = _editingCommentId + + val editingCommentContent: LiveData = _editingCommentId.map { commentId -> + if (commentId == null) null else _comments.value[commentId]?.comment?.content + } + + private val _canSubmitComment = NotNullMutableLiveData(true) + val canSubmitComment: NotNullLiveData = _canSubmitComment + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent + + init { + fetchComments() + } + + private fun fetchComments(): Job = fetchData( + fetchData = { commentRepository.getComment(parentCommentId) }, + onSuccess = { _comments.value = CommentsUiState(uid, it) }, + onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.IllegalCommentFetch }, + ) + + fun postChildComment(content: String): Job = commandAndRefresh( + command = { commentRepository.saveComment(content, feedId, parentCommentId) }, + onSuccess = { _uiEvent.value = ChildCommentsUiEvent.CommentPostComplete }, + onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.CommentPostFail }, + onStart = { _canSubmitComment.value = false }, + onFinish = { _canSubmitComment.value = true }, + ) + + fun updateComment(commentId: Long, content: String): Job = commandAndRefresh( + command = { commentRepository.updateComment(commentId, content) }, + onSuccess = { _editingCommentId.value = null }, + onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.CommentUpdateFail }, + onStart = { _canSubmitComment.value = false }, + onFinish = { _canSubmitComment.value = true }, + ) + + fun deleteComment(commentId: Long): Job = commandAndRefresh( + command = { commentRepository.deleteComment(commentId) }, + onFailure = { _, _ -> _uiEvent.value = ChildCommentsUiEvent.CommentDeleteFail }, + ) + + fun setEditMode(isEditMode: Boolean, commentId: Long = INVALID_COMMENT_ID) { + _editingCommentId.value = if (isEditMode) commentId else null + } + + fun reportComment(commentId: Long): Job = command( + command = { + val authorId = _comments.value.commentUiStates + .find { it.comment.id == commentId }!! + .comment.writer.id + commentRepository.reportComment(commentId, authorId, uid) + }, + onSuccess = { _uiEvent.value = ChildCommentsUiEvent.CommentReportComplete }, + onFailure = { code, _ -> + if (code == REPORT_DUPLICATE_ERROR_CODE) { + _uiEvent.value = ChildCommentsUiEvent.CommentReportDuplicate + } else { + _uiEvent.value = ChildCommentsUiEvent.CommentReportFail + } + }, + ) + + override fun refresh(): Job = refreshData( + refresh = { commentRepository.getComment(parentCommentId) }, + onSuccess = { _comments.value = CommentsUiState(uid, it) }, + ) + + fun highlight(commentId: Long) { + val comment = _comments.value.commentUiStates.find { it.comment.id == commentId } ?: return + if (comment.isHighlight) return + _comments.value = _comments.value.highlight(commentId) + } + + fun unhighlight(commentId: Long) { + val comment = _comments.value.commentUiStates.find { it.comment.id == commentId } ?: return + if (!comment.isHighlight) return + _comments.value = _comments.value.unhighlight() + } + + companion object { + const val KEY_FEED_ID = "KEY_FEED_ID" + const val KEY_PARENT_COMMENT_ID = "KEY_PARENT_COMMENT_ID" + + private const val INVALID_COMMENT_ID: Long = -1 + + private const val REPORT_DUPLICATE_ERROR_CODE = 400 + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentDiffUtil.kt similarity index 87% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentDiffUtil.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentDiffUtil.kt index 6c3207778..eb8a73b21 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentDiffUtil.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentDiffUtil.kt @@ -1,4 +1,4 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView +package com.emmsale.presentation.ui.childCommentList.recyclerView import androidx.recyclerview.widget.DiffUtil import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentsAdapter.kt similarity index 91% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentsAdapter.kt index a8e32b011..081c67b66 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/CommentsAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/recyclerView/CommentsAdapter.kt @@ -1,4 +1,4 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView +package com.emmsale.presentation.ui.childCommentList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter @@ -9,7 +9,7 @@ import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState class CommentsAdapter( private val onCommentClick: (comment: Comment) -> Unit, private val onAuthorImageClick: (authorId: Long) -> Unit, - private val onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, + private val onCommentMenuClick: (isWrittenByLoginUser: Boolean, comment: Comment) -> Unit, ) : ListAdapter(CommentDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder = CommentViewHolder( diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt deleted file mode 100644 index 615e781f5..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/childCommentList/uiState/ChildCommentsUiState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.emmsale.presentation.ui.childCommentList.uiState - -import com.emmsale.data.model.Comment -import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState - -data class ChildCommentsUiState(val comments: List = emptyList()) { - - fun highlight(commentId: Long) = copy( - comments = comments.map { if (it.comment.id == commentId) it.highlight() else it }, - ) - - fun unhighlight(commentId: Long) = copy( - comments = comments.map { if (it.comment.id == commentId) it.unhighlight() else it }, - ) - - companion object { - fun create( - uid: Long, - parentComment: Comment, - ) = ChildCommentsUiState( - comments = listOf(CommentUiState.create(uid, parentComment)) + - parentComment.childComments.map { CommentUiState.create(uid, it) }, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/competitionList/CompetitionFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/competitionList/CompetitionFragment.kt index 40db18977..897e14017 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/competitionList/CompetitionFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/competitionList/CompetitionFragment.kt @@ -22,8 +22,9 @@ import dagger.hilt.android.AndroidEntryPoint import java.time.LocalDate @AndroidEntryPoint -class CompetitionFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_competition +class CompetitionFragment : + BaseFragment(R.layout.fragment_competition) { + private val viewModel: CompetitionViewModel by viewModels() private val eventAdapter: CompetitionRecyclerViewAdapter by lazy { CompetitionRecyclerViewAdapter(::navigateToEventDetail) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/conferenceList/ConferenceFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/conferenceList/ConferenceFragment.kt index c848034ff..95c575af2 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/conferenceList/ConferenceFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/conferenceList/ConferenceFragment.kt @@ -22,8 +22,8 @@ import dagger.hilt.android.AndroidEntryPoint import java.time.LocalDate @AndroidEntryPoint -class ConferenceFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_conference +class ConferenceFragment : BaseFragment(R.layout.fragment_conference) { + private val viewModel: ConferenceViewModel by viewModels() private val eventAdapter by lazy { ConferenceRecyclerViewAdapter(::navigateToEventDetail) } private val filterActivityLauncher = diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileActivity.kt index 285b366f8..eaebe8d12 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileActivity.kt @@ -7,31 +7,33 @@ import android.text.Editable import android.text.InputType import android.text.TextWatcher import android.view.inputmethod.EditorInfo +import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.emmsale.R import com.emmsale.databinding.ActivityEditMyProfileBinding +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.dp import com.emmsale.presentation.common.extension.navigateToApplicationDetailSetting import com.emmsale.presentation.common.extension.showPermissionRequestDialog import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.imageUtil.getImageFileFromUri import com.emmsale.presentation.common.imageUtil.isImagePermissionGrantedCompat import com.emmsale.presentation.common.imageUtil.onImagePermissionCompat +import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.ui.editMyProfile.recyclerView.ActivitiesAdapter -import com.emmsale.presentation.ui.editMyProfile.recyclerView.ActivitiesAdapterDecoration import com.emmsale.presentation.ui.editMyProfile.recyclerView.FieldsAdapter -import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileErrorEvent +import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiEvent import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiState -import com.emmsale.presentation.ui.login.LoginActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class EditMyProfileActivity : AppCompatActivity() { - private val binding by lazy { ActivityEditMyProfileBinding.inflate(layoutInflater) } - private val viewModel: EditMyProfileViewModel by viewModels() +class EditMyProfileActivity : + NetworkActivity(R.layout.activity_edit_my_profile) { + + override val viewModel: EditMyProfileViewModel by viewModels() private val fieldsDialog by lazy { FieldsAddBottomDialogFragment() } private val educationsDialog by lazy { EducationsAddBottomDialogFragment() } @@ -57,27 +59,6 @@ class EditMyProfileActivity : AppCompatActivity() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - - initDataBinding() - initToolbar() - initDescriptionEditText() - initActivitiesRecyclerViews() - initFieldsRecyclerView() - setupUiLogic() - } - - private fun initDataBinding() { - binding.viewModel = viewModel - binding.lifecycleOwner = this - binding.showFieldTags = ::showFieldTags - binding.showEducations = ::showEducations - binding.showClubs = ::showClubs - binding.editProfileImage = ::editProfileImage - } - private fun navigateToGallery() { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "image/*" @@ -90,21 +71,27 @@ class EditMyProfileActivity : AppCompatActivity() { ) } - private fun editProfileImage() { - onImagePermissionCompat( - onGranted = ::navigateToGallery, - onShouldShowRequestPermissionRationale = { showNavigateToDetailSettingDialog() }, - onDenied = { imagePermissionLauncher.launch(it) }, - ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + setupDescriptionEditText() + setupActivitiesRecyclerViews() + setupFieldsRecyclerView() + + observeProfile() + observeUiEvent() } - private fun showNavigateToDetailSettingDialog() { - showPermissionRequestDialog( - message = getString(R.string.all_image_permission_request_dialog_message), - title = getString(R.string.all_image_permission_request_dialog_title), - onConfirm = { navigateToApplicationDetailSetting(permissionSettingLauncher) }, - onDenied = {}, - ) + private fun setupDataBinding() { + binding.viewModel = viewModel + binding.onFieldTagsAddButtonClick = ::showFieldTags + binding.onEducationAddButtonClick = ::showEducations + binding.onClubAddButtonClick = ::showClubs + binding.onProfileImageUpdateUiClick = ::startToEditProfileImage } private fun showFieldTags() { @@ -125,11 +112,40 @@ class EditMyProfileActivity : AppCompatActivity() { } } - private fun initToolbar() { - binding.tbEditmyprofileToolbar.setNavigationOnClickListener { finish() } + private fun startToEditProfileImage() { + onImagePermissionCompat( + onGranted = ::navigateToGallery, + onShouldShowRequestPermissionRationale = { showNavigateToDetailSettingDialog() }, + onDenied = { imagePermissionLauncher.launch(it) }, + ) + } + + private fun showNavigateToDetailSettingDialog() { + showPermissionRequestDialog( + message = getString(R.string.all_image_permission_request_dialog_message), + title = getString(R.string.all_image_permission_request_dialog_title), + onConfirm = { navigateToApplicationDetailSetting(permissionSettingLauncher) }, + onDenied = {}, + ) } - private fun initDescriptionEditText() { + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(RESULT_OK) + finish() + } + }, + ) + } + + private fun setupToolbar() { + binding.tbEditmyprofileToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + } + + private fun setupDescriptionEditText() { binding.etEditmyprofileDescription.imeOptions = EditorInfo.IME_ACTION_DONE binding.etEditmyprofileDescription.setRawInputType(InputType.TYPE_CLASS_TEXT) binding.etEditmyprofileDescription.addTextChangedListener( @@ -168,29 +184,21 @@ class EditMyProfileActivity : AppCompatActivity() { } } - private fun initFieldsRecyclerView() { - binding.rvEditmyprofileFields.adapter = FieldsAdapter(::removeField) - } - - private fun removeField(activityId: Long) { - viewModel.removeActivity(activityId) - } - - private fun initActivitiesRecyclerViews() { - val decoration = ActivitiesAdapterDecoration() + private fun setupActivitiesRecyclerViews() { + val decoration = IntervalItemDecoration(height = 2.dp) listOf( binding.rvEditmyprofileClubs, binding.rvEditmyprofileEducations, ).forEach { it.apply { - adapter = ActivitiesAdapter(::removeActivity) + adapter = ActivitiesAdapter(::showActivityRemoveConfirmDialog) itemAnimator = null addItemDecoration(decoration) } } } - private fun removeActivity(activityId: Long) { + private fun showActivityRemoveConfirmDialog(activityId: Long) { WarningDialog( context = this, title = getString(R.string.editmyprofile_activity_remove_warning_title), @@ -201,29 +209,16 @@ class EditMyProfileActivity : AppCompatActivity() { ).show() } - private fun setupUiLogic() { - setupLoginUiLogic() - setupProfileUiLogic() - setupErrorsUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(this) { - handleNotLogin(it) - } + private fun setupFieldsRecyclerView() { + binding.rvEditmyprofileFields.adapter = FieldsAdapter(::removeField) } - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(this) - finish() - } + private fun removeField(activityId: Long) { + viewModel.removeActivity(activityId) } - private fun setupProfileUiLogic() { - viewModel.profile.observe(this) { - handleActivities(it) - } + private fun observeProfile() { + viewModel.profile.observe(this, ::handleActivities) } private fun handleActivities(profile: EditMyProfileUiState) { @@ -232,26 +227,24 @@ class EditMyProfileActivity : AppCompatActivity() { (binding.rvEditmyprofileEducations.adapter as ActivitiesAdapter).submitList(profile.member.educations) } - private fun setupErrorsUiLogic() { - viewModel.errorEvents.observe(this) { - handleErrors(it) - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun handleErrors(errorEvent: EditMyProfileErrorEvent?) { - when (errorEvent) { - EditMyProfileErrorEvent.DESCRIPTION_UPDATE -> binding.root.showSnackBar(getString(R.string.editmyprofile_update_description_error_message)) - EditMyProfileErrorEvent.ACTIVITY_REMOVE -> binding.root.showSnackBar(getString(R.string.editmyprofile_activity_remove_error_message)) - EditMyProfileErrorEvent.ACTIVITIES_ADD -> binding.root.showSnackBar(getString(R.string.editmyprofile_acitivities_add_error_message)) - EditMyProfileErrorEvent.PROFILE_IMAGE_UPDATE -> binding.root.showSnackBar(getString(R.string.editmyprofile_update_profile_image_error_message)) - else -> return + private fun handleUiEvent(uiEvent: EditMyProfileUiEvent) { + when (uiEvent) { + EditMyProfileUiEvent.ActivitiesAddFail -> binding.root.showSnackBar(getString(R.string.editmyprofile_acitivities_add_error_message)) + EditMyProfileUiEvent.ActivityRemoveFail -> binding.root.showSnackBar(getString(R.string.editmyprofile_activity_remove_error_message)) + EditMyProfileUiEvent.DescriptionUpdateFail -> binding.root.showSnackBar(getString(R.string.editmyprofile_update_description_error_message)) + EditMyProfileUiEvent.ProfileImageUpdateFail -> binding.root.showSnackBar(getString(R.string.editmyprofile_update_profile_image_error_message)) } - viewModel.removeError() } companion object { fun startActivity(context: Context) { - context.startActivity(Intent(context, EditMyProfileActivity::class.java)) + context.startActivity(getIntent(context)) } + + fun getIntent(context: Context) = Intent(context, EditMyProfileActivity::class.java) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileViewModel.kt index 192a9cf0e..c8245e398 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/EditMyProfileViewModel.kt @@ -1,23 +1,20 @@ package com.emmsale.presentation.ui.editMyProfile import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Member import com.emmsale.data.repository.interfaces.ActivityRepository import com.emmsale.data.repository.interfaces.MemberRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.editMyProfile.uiState.ActivitiesUiState -import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileErrorEvent +import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiEvent import com.emmsale.presentation.ui.editMyProfile.uiState.EditMyProfileUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @@ -27,110 +24,59 @@ class EditMyProfileViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val memberRepository: MemberRepository, private val activityRepository: ActivityRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin + private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _profile = NotNullMutableLiveData(EditMyProfileUiState.FIRST_LOADING) + private val _profile = NotNullMutableLiveData(EditMyProfileUiState(Member())) val profile: NotNullLiveData = _profile - private val _errorEvents = MutableLiveData(null) - val errorEvents: LiveData = _errorEvents + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent private val _activities = NotNullMutableLiveData(ActivitiesUiState()) val activities: NotNullLiveData = _activities init { - refresh() + fetchProfile() } - override fun refresh() { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return - } - _profile.value = _profile.value.copy(isLoading = false) - viewModelScope.launch { - when (val result = memberRepository.getMember(token.uid)) { - is Success -> _profile.value = _profile.value.changeMemberState(result.data) - is Failure, NetworkError -> _profile.value = _profile.value.changeToErrorState() - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun fetchUnselectedActivities() { - viewModelScope.launch { - when (val result = activityRepository.getActivities()) { - is Success -> _activities.value = _activities.value.fetchUnselectedActivities( - allActivities = result.data, - myActivities = profile.value.member.activities, - ) - - is Failure, NetworkError -> - _activities.value = - _activities.value.changeToErrorState() - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun updateProfileImage(profileImageFile: File) { - _profile.value = _profile.value.copy(isLoading = true) - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - when ( - val result = - memberRepository.updateMemberProfileImage(token.uid, profileImageFile) - ) { - is Success -> _profile.value = _profile.value.updateProfileImageUrl(result.data) - - is Failure, NetworkError -> - _errorEvents.value = EditMyProfileErrorEvent.PROFILE_IMAGE_UPDATE - - is Unexpected -> throw Throwable(result.error) - } - _profile.value = _profile.value.copy(isLoading = false) - } - } - - fun updateDescription(description: String) { - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - when (val result = memberRepository.updateMemberDescription(description)) { - is Failure, NetworkError -> - _errorEvents.value = - EditMyProfileErrorEvent.DESCRIPTION_UPDATE - - is Success -> _profile.value = _profile.value.changeDescription(description) - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun removeActivity(activityId: Long) { - viewModelScope.launch { - when (val result = memberRepository.deleteMemberActivities(listOf(activityId))) { - is Failure, NetworkError -> - _errorEvents.value = - EditMyProfileErrorEvent.ACTIVITY_REMOVE - - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } + private fun fetchProfile(): Job = fetchData( + fetchData = { memberRepository.getMember(uid) }, + onSuccess = { _profile.value = EditMyProfileUiState(it) }, + ) + + override fun refresh(): Job = refreshData( + refresh = { memberRepository.getMember(uid) }, + onSuccess = { _profile.value = EditMyProfileUiState(it) }, + ) + + fun fetchUnselectedActivities(): Job = fetchData( + fetchData = { activityRepository.getActivities() }, + onSuccess = { + _activities.value = _activities.value.fetchUnselectedActivities( + allActivities = it, + myActivities = profile.value.member.activities, + ) + }, + ) + + fun updateProfileImage(profileImageFile: File): Job = command( + command = { memberRepository.updateMemberProfileImage(uid, profileImageFile) }, + onSuccess = { _profile.value = _profile.value.updateProfileImageUrl(it) }, + onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ProfileImageUpdateFail }, + ) + + fun updateDescription(description: String): Job = command( + command = { memberRepository.updateMemberDescription(description) }, + onSuccess = { _profile.value = _profile.value.changeDescription(description) }, + onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.DescriptionUpdateFail }, + ) + + fun removeActivity(activityId: Long): Job = commandAndRefresh( + command = { memberRepository.deleteMemberActivities(listOf(activityId)) }, + onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ActivityRemoveFail }, + ) fun addSelectedFields() { viewModelScope.launch { @@ -156,19 +102,12 @@ class EditMyProfileViewModel @Inject constructor( } } - private suspend fun updateMemberActivities(activityIds: List) { - when (val result = memberRepository.addMemberActivities(activityIds)) { - is Failure, NetworkError -> _errorEvents.value = EditMyProfileErrorEvent.ACTIVITIES_ADD - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } + private suspend fun updateMemberActivities(activityIds: List): Job = commandAndRefresh( + command = { memberRepository.addMemberActivities(activityIds) }, + onFailure = { _, _ -> _uiEvent.value = EditMyProfileUiEvent.ActivitiesAddFail }, + ) fun toggleActivitySelection(activityId: Long) { _activities.value = activities.value.toggleIsSelected(activityId) } - - fun removeError() { - _errorEvents.value = null - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/ActivitiesUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/ActivitiesUiState.kt index 6f9496285..5424919cb 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/ActivitiesUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/ActivitiesUiState.kt @@ -6,8 +6,6 @@ import com.emmsale.data.model.ActivityType.EDUCATION import com.emmsale.data.model.ActivityType.INTEREST_FIELD data class ActivitiesUiState( - val isLoading: Boolean = false, - val isError: Boolean = false, val activities: List = emptyList(), ) { val fields = activities.filter { it.activity.activityType == INTEREST_FIELD } @@ -30,23 +28,9 @@ data class ActivitiesUiState( .filterNot { myActivities.contains(it) } .map { ActivityUiState(it, false) } - return copy( - isLoading = false, - isError = false, - activities = unSelectedActivities, - ) + return ActivitiesUiState(unSelectedActivities) } - fun changeToErrorState(): ActivitiesUiState = copy( - isLoading = false, - isError = true, - ) - - fun changeToLoadingState(): ActivitiesUiState = copy( - isLoading = true, - isError = false, - ) - fun toggleIsSelected(activityId: Long): ActivitiesUiState = copy( activities = activities.map { if (it.activity.id == activityId) it.toggleSelection() else it }, ) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileErrorEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileErrorEvent.kt deleted file mode 100644 index dcece73ca..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileErrorEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.emmsale.presentation.ui.editMyProfile.uiState - -enum class EditMyProfileErrorEvent { - DESCRIPTION_UPDATE, - ACTIVITY_REMOVE, - ACTIVITIES_ADD, - PROFILE_IMAGE_UPDATE, -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiEvent.kt new file mode 100644 index 000000000..5cfb855fd --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiEvent.kt @@ -0,0 +1,8 @@ +package com.emmsale.presentation.ui.editMyProfile.uiState + +sealed interface EditMyProfileUiEvent { + object DescriptionUpdateFail : EditMyProfileUiEvent + object ActivityRemoveFail : EditMyProfileUiEvent + object ActivitiesAddFail : EditMyProfileUiEvent + object ProfileImageUpdateFail : EditMyProfileUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiState.kt index c81fef64c..9e2fcffaa 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/editMyProfile/uiState/EditMyProfileUiState.kt @@ -3,28 +3,12 @@ package com.emmsale.presentation.ui.editMyProfile.uiState import com.emmsale.data.model.Member data class EditMyProfileUiState( - val isLoading: Boolean = true, - val isError: Boolean = false, val member: Member = Member(), ) { val selectableFieldSize: Int get() = (MAX_FIELDS_COUNT - member.fields.size).coerceAtLeast(0) - fun changeMemberState(member: Member): EditMyProfileUiState = copy( - isLoading = false, - isError = false, - member = member, - ) - - fun changeToLoadingState(): EditMyProfileUiState = copy( - isLoading = true, - ) - - fun changeToErrorState(): EditMyProfileUiState = copy( - isError = true, - ) - fun changeDescription(description: String): EditMyProfileUiState = copy( member = member.copy(description = description), ) @@ -34,12 +18,6 @@ data class EditMyProfileUiState( ) companion object { - val FIRST_LOADING = EditMyProfileUiState( - isLoading = true, - isError = false, - member = Member(), - ) - private const val MAX_FIELDS_COUNT = 4 } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailActivity.kt index f12668014..2cbd89ef0 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailActivity.kt @@ -6,102 +6,69 @@ import android.net.Uri import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityEventDetailBinding -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl import com.emmsale.presentation.ui.eventDetail.EventDetailViewModel.Companion.EVENT_ID_KEY import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailScreenUiState -import com.emmsale.presentation.ui.eventDetailInfo.uiState.EventInfoUiEvent +import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailUiEvent import com.emmsale.presentation.ui.feedWriting.FeedWritingActivity import com.emmsale.presentation.ui.main.MainActivity -import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingActivity +import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentWritingActivity import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class EventDetailActivity : - AppCompatActivity(), + NetworkActivity(R.layout.activity_event_detail), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("event_detail") { - private val binding by lazy { ActivityEventDetailBinding.inflate(layoutInflater) } - private val viewModel: EventDetailViewModel by viewModels() + + override val viewModel: EventDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentView(binding.root) + registerScreen(this) - initFragmentStateAdapter() - initBackPressedDispatcher() - setUpBinding() - setUpScrapUiEvent() - setUpRecruitmentWritingPermission() - initBackPressButtonClickListener() - onTabSelectedListener() - } - private fun initBackPressedDispatcher() { - onBackPressedDispatcher.addCallback(this, EventDetailOnBackPressedCallback()) + setupDataBinding() + setupFragmentStateAdapter() + setupBackPressedDispatcher() + setupToolbar() + setupOnTabSelectedListener() + + observeUiEvent() } - private fun setUpBinding() { - setContentView(binding.root) - binding.lifecycleOwner = this + private fun setupDataBinding() { binding.vm = viewModel binding.navigateToUrl = ::navigateToUrl binding.navigateToWritingPost = ::navigateToWriting } - private fun setUpScrapUiEvent() { - viewModel.scrapUiEvent.observe(this) { event -> - handleEvent(event) - } - } - - private fun handleEvent(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content) { - EventInfoUiEvent.SCRAP_ERROR -> binding.root.showSnackBar("스크랩 불가") - EventInfoUiEvent.SCRAP_DELETE_ERROR -> binding.root.showSnackBar("스크랩 삭제 불가") - } - } - - private fun setUpRecruitmentWritingPermission() { - viewModel.hasWritingPermission.observe(this) { - val hasPermission = it.getContentIfNotHandled() ?: return@observe - if (hasPermission) { - navigateToRecruitmentWriting() - } else { - binding.root.showSnackBar(getString(R.string.eventrecruitment_has_not_permission_writing)) - } - } - } - - private fun navigateToRecruitmentWriting() { - startActivity(RecruitmentPostWritingActivity.getPostModeIntent(this, viewModel.eventId)) + private fun navigateToUrl(url: String) { + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(url), + ) + startActivity(browserIntent) } private fun navigateToWriting() { when (viewModel.currentScreen.value) { EventDetailScreenUiState.INFORMATION -> Unit - EventDetailScreenUiState.RECRUITMENT -> viewModel.fetchHasWritingPermission() + EventDetailScreenUiState.RECRUITMENT -> viewModel.checkIsAlreadyPostRecruitment() EventDetailScreenUiState.POST -> { FeedWritingActivity.startActivity(this, viewModel.eventId) } } } - private fun navigateToUrl(url: String) { - val browserIntent = Intent( - Intent.ACTION_VIEW, - Uri.parse(url), - ) - startActivity(browserIntent) - } - - private fun initFragmentStateAdapter() { + private fun setupFragmentStateAdapter() { binding.vpEventdetail.adapter = EventDetailFragmentStateAdapter(this, viewModel.eventId) val tabNames = listOf( @@ -115,7 +82,23 @@ class EventDetailActivity : binding.vpEventdetail.isUserInputEnabled = false } - private fun onTabSelectedListener() { + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + if (isTaskRoot) MainActivity.startActivity(this@EventDetailActivity) + } + }, + ) + } + + private fun setupToolbar() { + binding.tbEventdetail.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + } + + private fun setupOnTabSelectedListener() { binding.tablayoutEventdetail.addOnTabSelectedListener( object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { @@ -128,8 +111,22 @@ class EventDetailActivity : ) } - private fun initBackPressButtonClickListener() { - binding.tbEventdetail.setNavigationOnClickListener { finish() } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: EventDetailUiEvent) { + when (uiEvent) { + EventDetailUiEvent.ScrapFail -> binding.root.showSnackBar(R.string.eventdetail_scrap_fail) + EventDetailUiEvent.ScrapOffFail -> binding.root.showSnackBar(R.string.eventdetail_scrap_off_fail) + EventDetailUiEvent.RecruitmentIsAlreadyPosted -> binding.root.showSnackBar(R.string.eventrecruitment_has_not_permission_writing) + EventDetailUiEvent.RecruitmentPostApproval -> navigateToRecruitmentWriting() + EventDetailUiEvent.RecruitmentPostedCheckFail -> binding.root.showSnackBar(R.string.eventrecruitment_has_not_permission_writing_check_fail_message) + } + } + + private fun navigateToRecruitmentWriting() { + startActivity(RecruitmentWritingActivity.getPostModeIntent(this, viewModel.eventId)) } companion object { @@ -142,11 +139,4 @@ class EventDetailActivity : fun getIntent(context: Context, eventId: Long): Intent = Intent(context, EventDetailActivity::class.java).putExtra(EVENT_ID_KEY, eventId) } - - inner class EventDetailOnBackPressedCallback : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - if (isTaskRoot) MainActivity.startActivity(this@EventDetailActivity) - } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailFragmentStateAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailFragmentStateAdapter.kt index 61447cdd5..03e3c0449 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailFragmentStateAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailFragmentStateAdapter.kt @@ -9,7 +9,7 @@ import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailScreenUiState. import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailScreenUiState.RECRUITMENT import com.emmsale.presentation.ui.eventDetailInfo.EventInfoFragment import com.emmsale.presentation.ui.feedList.FeedListFragment -import com.emmsale.presentation.ui.recruitmentList.EventRecruitmentFragment +import com.emmsale.presentation.ui.recruitmentList.RecruitmentsFragment class EventDetailFragmentStateAdapter( fragmentActivity: FragmentActivity, @@ -22,7 +22,7 @@ class EventDetailFragmentStateAdapter( return when (EventDetailScreenUiState.from(position)) { INFORMATION -> EventInfoFragment.create() POST -> FeedListFragment.create(eventId) - RECRUITMENT -> EventRecruitmentFragment.create(eventId) + RECRUITMENT -> RecruitmentsFragment.create(eventId) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailViewModel.kt index c1abeeb28..bb369b49e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/EventDetailViewModel.kt @@ -1,29 +1,21 @@ package com.emmsale.presentation.ui.eventDetail import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.model.Event import com.emmsale.data.repository.interfaces.EventRepository import com.emmsale.data.repository.interfaces.RecruitmentRepository -import com.emmsale.presentation.common.FetchResult.ERROR -import com.emmsale.presentation.common.FetchResult.LOADING -import com.emmsale.presentation.common.FetchResult.SUCCESS -import com.emmsale.presentation.common.UiEvent -import com.emmsale.presentation.common.firebase.analytics.logEventClick +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailScreenUiState -import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailUiState -import com.emmsale.presentation.ui.eventDetailInfo.uiState.EventInfoUiEvent +import com.emmsale.presentation.ui.eventDetail.uiState.EventDetailUiEvent import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,108 +24,98 @@ class EventDetailViewModel @Inject constructor( stateHandle: SavedStateHandle, private val eventRepository: EventRepository, private val recruitmentRepository: RecruitmentRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { val eventId = stateHandle[EVENT_ID_KEY] ?: DEFAULT_EVENT_ID - private val _eventDetail: NotNullMutableLiveData = - NotNullMutableLiveData(EventDetailUiState()) - val eventDetail: NotNullLiveData = _eventDetail + private val _event = NotNullMutableLiveData(Event()) + val event: NotNullLiveData = _event - private val _scrapUiEvent = MutableLiveData>() - val scrapUiEvent: LiveData> = _scrapUiEvent + private val _isScraped = NotNullMutableLiveData(false) + val isScraped: NotNullLiveData = _isScraped - private val _isScraped: MutableLiveData = MutableLiveData(false) - val isScraped: LiveData = _isScraped + private val _canChangeIsScrapped = NotNullMutableLiveData(true) + val canChangeIsScrapped: NotNullLiveData = _canChangeIsScrapped private val _currentScreen = NotNullMutableLiveData(EventDetailScreenUiState.INFORMATION) val currentScreen: NotNullLiveData = _currentScreen - private val _hasWritingPermission: MutableLiveData> = MutableLiveData() - val hasWritingPermission: LiveData> = _hasWritingPermission - - init { - refresh() - } - - override fun refresh() { - changeToLoadingState() - fetchEventDetail() - fetchIsScrapped() - } - - private fun fetchEventDetail() { - viewModelScope.launch { - when (val eventFetchResult = eventRepository.getEventDetail(eventId)) { - is Success -> changeToSuccessState(eventFetchResult.data) - is Failure, NetworkError, is Unexpected -> changeToErrorState() - } - } + private val _canStartToWriteRecruitment = NotNullMutableLiveData(true) + val canStartToWrite: LiveData = MediatorLiveData().apply { + addSource(_canStartToWriteRecruitment) { value = canStartToWrite() } + addSource(_currentScreen) { value = canStartToWrite() } } - fun fetchCurrentScreen(position: Int) { - _currentScreen.value = EventDetailScreenUiState.from(position) - } + private fun canStartToWrite(): Boolean = + _currentScreen.value != EventDetailScreenUiState.RECRUITMENT || _canStartToWriteRecruitment.value - private fun fetchIsScrapped() { - viewModelScope.launch { - when (val isScrappedFetchResult = eventRepository.isScraped(eventId)) { - is Success -> _isScraped.value = isScrappedFetchResult.data - is Failure, NetworkError, is Unexpected -> {} - } - } - } + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - fun handleEventScrap() { - when (isScraped.value) { - null, true -> deleteScrap() - false -> scrapEvent() - } + init { + fetchEvent() + fetchIsScrapped() } - private fun scrapEvent() { - viewModelScope.launch { - when (eventRepository.scrapEvent(eventId = eventId)) { - is Success -> _isScraped.value = true - else -> _scrapUiEvent.value = UiEvent(EventInfoUiEvent.SCRAP_ERROR) - } - } - } + private fun fetchEvent(): Job = fetchData( + fetchData = { eventRepository.getEventDetail(eventId) }, + onSuccess = { _event.value = it }, + ) - private fun deleteScrap() { - viewModelScope.launch { - when (eventRepository.deleteScrap(eventId = eventId)) { - is Success -> _isScraped.value = false - else -> _scrapUiEvent.value = UiEvent(EventInfoUiEvent.SCRAP_DELETE_ERROR) - } + private fun fetchIsScrapped(): Job = viewModelScope.launch { + when (val result = eventRepository.isScraped(eventId)) { + is Success -> _isScraped.value = result.data + else -> {} } } - private fun changeToSuccessState(event: Event) { - _eventDetail.value = - _eventDetail.value.copy(fetchResult = SUCCESS, eventDetail = event) - logEventClick(event.name, event.id) + override fun refresh(): Job { + fetchIsScrapped() + return refreshEvent() } - private fun changeToLoadingState() { - _eventDetail.value = _eventDetail.value.copy(fetchResult = LOADING) - } + private fun refreshEvent(): Job = refreshData( + refresh = { eventRepository.getEventDetail(eventId) }, + onSuccess = { _event.value = it }, + ) - private fun changeToErrorState() { - _eventDetail.value = _eventDetail.value.copy(fetchResult = ERROR) + fun fetchCurrentScreen(position: Int) { + _currentScreen.value = EventDetailScreenUiState.from(position) } - fun fetchHasWritingPermission() { - viewModelScope.launch { - when (val response = recruitmentRepository.checkIsAlreadyPostRecruitment(eventId)) { - is Success -> setHasPermissionWritingState(!response.data) - else -> setHasPermissionWritingState(false) + fun toggleIsScrapped(): Job = command( + command = { + if (isScraped.value) { + eventRepository.scrapOffEvent(eventId) + } else { + eventRepository.scrapEvent(eventId) } - } - } - - private fun setHasPermissionWritingState(state: Boolean) { - _hasWritingPermission.value = UiEvent(state) - } + }, + onSuccess = { _isScraped.value = !_isScraped.value }, + onFailure = { _, _ -> + if (isScraped.value) { + _uiEvent.value = EventDetailUiEvent.ScrapOffFail + } else { + _uiEvent.value = EventDetailUiEvent.ScrapFail + } + }, + onStart = { _canChangeIsScrapped.value = false }, + onFinish = { _canChangeIsScrapped.value = true }, + ) + + fun checkIsAlreadyPostRecruitment(): Job = fetchData( + fetchData = { recruitmentRepository.checkIsAlreadyPostRecruitment(eventId) }, + onSuccess = { isAlreadyPosted -> + _uiEvent.value = if (isAlreadyPosted) { + EventDetailUiEvent.RecruitmentIsAlreadyPosted + } else { + EventDetailUiEvent.RecruitmentPostApproval + } + }, + onFailure = { _, _ -> _uiEvent.value = EventDetailUiEvent.RecruitmentPostedCheckFail }, + onLoading = { delayLoading() }, + onStart = { _canStartToWriteRecruitment.value = false }, + onFinish = { _canStartToWriteRecruitment.value = true }, + ) companion object { const val EVENT_ID_KEY = "EVENT_ID_KEY" diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiEvent.kt new file mode 100644 index 000000000..323ede06f --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiEvent.kt @@ -0,0 +1,9 @@ +package com.emmsale.presentation.ui.eventDetail.uiState + +sealed interface EventDetailUiEvent { + object ScrapFail : EventDetailUiEvent + object ScrapOffFail : EventDetailUiEvent + object RecruitmentPostApproval : EventDetailUiEvent + object RecruitmentIsAlreadyPosted : EventDetailUiEvent + object RecruitmentPostedCheckFail : EventDetailUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiState.kt deleted file mode 100644 index 4812bf04a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetail/uiState/EventDetailUiState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.emmsale.presentation.ui.eventDetail.uiState - -import com.emmsale.data.model.Event -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class EventDetailUiState( - override val fetchResult: FetchResult = FetchResult.SUCCESS, - val eventDetail: Event? = null, -) : FetchResultUiState() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetailInfo/EventInfoFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetailInfo/EventInfoFragment.kt index 367fce570..f90c104c4 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetailInfo/EventInfoFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventDetailInfo/EventInfoFragment.kt @@ -15,9 +15,9 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class EventInfoFragment : - BaseFragment(), + BaseFragment(R.layout.fragment_event_information), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("event_recruitment") { - override val layoutResId: Int = R.layout.fragment_event_information + private val viewModel: EventDetailViewModel by activityViewModels() override fun onAttach(context: Context) { @@ -32,10 +32,9 @@ class EventInfoFragment : } private fun setUpInformationUrls() { - viewModel.eventDetail.observe(viewLifecycleOwner) { eventDetailUiState -> + viewModel.event.observe(viewLifecycleOwner) { event -> binding.rvEventInfoImages.setHasFixedSize(true) - val detailImageUrls = eventDetailUiState.eventDetail?.detailImageUrls ?: return@observe - binding.rvEventInfoImages.adapter = EventInfoImageAdapter(detailImageUrls) + binding.rvEventInfoImages.adapter = EventInfoImageAdapter(event.detailImageUrls) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventPageList/EventFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventPageList/EventFragment.kt index 4ddf0195e..fa25387d8 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventPageList/EventFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/eventPageList/EventFragment.kt @@ -6,18 +6,21 @@ import com.emmsale.R import com.emmsale.databinding.FragmentEventBinding import com.emmsale.presentation.base.BaseFragment import com.emmsale.presentation.ui.eventSearch.EventSearchActivity -import com.emmsale.presentation.ui.notificationPageList.NotificationBoxActivity +import com.emmsale.presentation.ui.notificationList.NotificationsActivity import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class EventFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_event +class EventFragment : BaseFragment(R.layout.fragment_event) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initView() + + selectTab() + setupEventSearchView() + setupEventViewPager() + setupNotificationView() } override fun onViewStateRestored(savedInstanceState: Bundle?) { @@ -26,34 +29,27 @@ class EventFragment : BaseFragment() { selectTab(selectedPosition) } - private fun initView() { - selectTab() - initEventViewPager() - initNotificationView() - initEventSearchView() + private fun selectTab(position: Int = CONFERENCE_TAB) { + binding.tlEvent.getTabAt(position)?.select() } - private fun initEventSearchView() { + private fun setupEventSearchView() { binding.btnEventSearch.setOnClickListener { EventSearchActivity.startActivity(requireContext()) } } - private fun selectTab(position: Int = CONFERENCE_TAB) { - binding.tlEvent.getTabAt(position)?.select() - } - - private fun initEventViewPager() { - initEventFragmentStateAdapter() - initEventTabLayoutMediator() - initEventTabLayoutSelectedListener() + private fun setupEventViewPager() { + setupEventFragmentStateAdapter() + setupEventTabLayoutMediator() + setupEventTabLayoutSelectedListener() } - private fun initEventFragmentStateAdapter() { + private fun setupEventFragmentStateAdapter() { binding.vpEvent.adapter = EventFragmentStateAdapter(this) } - private fun initEventTabLayoutMediator() { + private fun setupEventTabLayoutMediator() { val eventTabNames = listOf( getString(R.string.event_scrap), getString(R.string.event_conference), @@ -65,7 +61,7 @@ class EventFragment : BaseFragment() { }.attach() } - private fun initEventTabLayoutSelectedListener() { + private fun setupEventTabLayoutSelectedListener() { binding.tlEvent.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { binding.vpEvent.currentItem = tab.position @@ -76,10 +72,10 @@ class EventFragment : BaseFragment() { }) } - private fun initNotificationView() { + private fun setupNotificationView() { binding.tbEventToolbar.setOnMenuItemClickListener { when (it.itemId) { - R.id.notification_button -> NotificationBoxActivity.startActivity(requireContext()) + R.id.notification_button -> NotificationsActivity.startActivity(requireContext()) } true } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt index bcde2d704..8308b5c0c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailActivity.kt @@ -3,116 +3,153 @@ package com.emmsale.presentation.ui.feedDetail import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.inputmethod.InputMethodManager import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearSmoothScroller import com.emmsale.R +import com.emmsale.data.model.Comment import com.emmsale.databinding.ActivityFeedDetailBinding -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.hideKeyboard import com.emmsale.presentation.common.extension.showKeyboard import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity +import com.emmsale.presentation.ui.childCommentList.ChildCommentsActivity import com.emmsale.presentation.ui.feedDetail.FeedDetailViewModel.Companion.KEY_FEED_ID -import com.emmsale.presentation.ui.feedDetail.recyclerView.CommentsAdapter -import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedDetailAdapter +import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedAndCommentsAdapter import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiEvent import com.emmsale.presentation.ui.profile.ProfileActivity import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @AndroidEntryPoint -class FeedDetailActivity : AppCompatActivity() { - private val binding by lazy { ActivityFeedDetailBinding.inflate(layoutInflater) } - - private val viewModel: FeedDetailViewModel by viewModels() +class FeedDetailActivity : + NetworkActivity(R.layout.activity_feed_detail) { private val highlightCommentId: Long by lazy { intent.getLongExtra(KEY_HIGHLIGHT_COMMENT_ID, INVALID_COMMENT_ID) } - private val inputMethodManager: InputMethodManager by lazy { - getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - } + override val viewModel: FeedDetailViewModel by viewModels() + private val bottomMenuDialog: BottomMenuDialog by lazy { BottomMenuDialog(this) } - private val feedDetailAdapter: FeedDetailAdapter = FeedDetailAdapter(::showProfile) - private val commentsAdapter: CommentsAdapter = CommentsAdapter( - onCommentClick = { comment -> - ChildCommentActivity.startActivity( - context = this, - feedId = comment.feed.id, - parentCommentId = comment.parentCommentId ?: comment.id, - highlightCommentId = comment.id, - ) - }, - onAuthorImageClick = ::showProfile, + private val feedAndCommentsAdapter = FeedAndCommentsAdapter( + onAuthorImageClick = ::navigateToProfile, + onCommentClick = ::navigateToChildComments, onCommentMenuClick = ::showCommentMenuDialog, - ).apply { - registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (highlightCommentId == INVALID_COMMENT_ID || viewModel.isAlreadyFirstFetched || itemCount == 0) return - val position = viewModel.feedDetail.value.comments - .indexOfFirst { it.comment.id == highlightCommentId } + FEED_DETAIL_COUNT - binding.rvFeeddetailFeedAndComments.scrollToPosition(position) + ) - viewModel.highlightComment(highlightCommentId) + private fun navigateToChildComments(comment: Comment) { + ChildCommentsActivity.startActivity( + context = this, + feedId = comment.feed.id, + parentCommentId = comment.parentCommentId ?: comment.id, + highlightCommentId = comment.id, + ) + } - viewModel.isAlreadyFirstFetched = true - } - }) + private fun navigateToProfile(authorId: Long) { + ProfileActivity.startActivity(this, authorId) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) + private fun showCommentMenuDialog(isWrittenByLoginUser: Boolean, comment: Comment) { + bottomMenuDialog.resetMenu() + if (isWrittenByLoginUser) { + bottomMenuDialog.addCommentUpdateButton(comment.id) + bottomMenuDialog.addCommentDeleteButton(comment.id) + } else { + bottomMenuDialog.addCommentReportButton(comment.id) + } + bottomMenuDialog.show() + } - setUpDataBinding() - setUpToolbar() - setUpRecyclerView() - setUpUiEvent() - setUpCommentEditing() - setUpFeedDetail() + private fun BottomMenuDialog.addCommentUpdateButton(commentId: Long) { + addMenuItemBelow(context.getString(R.string.all_update_button_label)) { + viewModel.startEditComment(commentId) + binding.stiwCommentUpdate.requestFocusOnEditText() + showKeyboard() + } } - private fun setUpDataBinding() { - binding.lifecycleOwner = this - binding.vm = viewModel - binding.postComment = ::onCommentSave - binding.cancelUpdateComment = ::cancelUpdateComment - binding.updateComment = ::updateComment + private fun BottomMenuDialog.addCommentDeleteButton(commentId: Long) { + addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { + showCommentDeleteDialog(commentId) + } } - private fun onCommentSave() { - viewModel.saveComment(binding.etCommentsPostComment.text.toString()) - binding.etCommentsPostComment.text.clear() - hideKeyboard() + private fun showCommentDeleteDialog(commentId: Long) { + WarningDialog( + context = this, + title = getString(R.string.commentdeletedialog_title), + message = getString(R.string.commentdeletedialog_message), + positiveButtonLabel = getString(R.string.commentdeletedialog_positive_button_label), + negativeButtonLabel = getString(R.string.commentdeletedialog_negative_button_label), + onPositiveButtonClick = { viewModel.deleteComment(commentId) }, + ).show() + } + + private fun BottomMenuDialog.addCommentReportButton(commentId: Long) { + addMenuItemBelow( + context.getString(R.string.all_report_button_label), + MenuItemType.IMPORTANT, + ) { showCommentReportConfirmDialog(commentId) } } - private fun hideKeyboard() { - inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0) + private fun showCommentReportConfirmDialog(commentId: Long) { + WarningDialog( + context = this, + title = getString(R.string.all_report_dialog_title), + message = getString(R.string.comments_comment_report_dialog_message), + positiveButtonLabel = getString(R.string.all_report_dialog_positive_button_label), + negativeButtonLabel = getString(R.string.commentdeletedialog_negative_button_label), + onPositiveButtonClick = { + viewModel.reportComment(commentId) + }, + ).show() } - private fun cancelUpdateComment() { - viewModel.setEditMode(false) - hideKeyboard() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setupDataBinding() + setupToolbar() + setupFeedAndCommentsRecyclerView() + + observeFeedDetail() + observeUiEvent() } - private fun updateComment() { - val commentId = viewModel.editingCommentId.value ?: return - val content = binding.etCommentsCommentUpdate.text.toString() - viewModel.updateComment(commentId, content) - hideKeyboard() + override fun onRestart() { + super.onRestart() + viewModel.refresh() } - private fun setUpToolbar() { - binding.tbFeeddetailToolbar.setNavigationOnClickListener { finish() } + private fun setupDataBinding() { + binding.vm = viewModel + binding.onCommentSubmitButtonClick = { + viewModel.postComment(it) + hideKeyboard() + } + binding.onCommentUpdateCancelButtonClick = { + viewModel.cancelEditComment() + hideKeyboard() + } + binding.onUpdatedCommentSubmitButtonClick = { + val commentId = viewModel.editingCommentId.value + if (commentId != null) viewModel.updateComment(commentId, it) + hideKeyboard() + } + } + + private fun setupToolbar() { + binding.tbFeeddetailToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.tbFeeddetailToolbar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.more -> showFeedDetailMenuDialog() @@ -140,11 +177,11 @@ class FeedDetailActivity : AppCompatActivity() { private fun BottomMenuDialog.addFeedDeleteButton() { addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { - onFeedDeleteButtonClick() + showFeedDeleteConfirmDialog() } } - private fun onFeedDeleteButtonClick() { + private fun showFeedDeleteConfirmDialog() { WarningDialog( context = this, title = getString(R.string.feeddetaildeletedialog_title), @@ -164,105 +201,34 @@ class FeedDetailActivity : AppCompatActivity() { } } - private fun setUpRecyclerView() { - val concatAdapterConfig = ConcatAdapter.Config.Builder().build() - - binding.rvFeeddetailFeedAndComments.apply { - adapter = ConcatAdapter( - concatAdapterConfig, - feedDetailAdapter, - commentsAdapter, - ) + private fun setupFeedAndCommentsRecyclerView() { + binding.rvFeedAndComments.apply { + adapter = feedAndCommentsAdapter itemAnimator = null addItemDecoration(DividerItemDecoration(this@FeedDetailActivity)) } } - private fun showProfile(authorId: Long) { - ProfileActivity.startActivity(this, authorId) - } - - private fun showCommentMenuDialog(isWrittenByLoginUser: Boolean, commentId: Long) { - bottomMenuDialog.resetMenu() - if (isWrittenByLoginUser) { - bottomMenuDialog.addCommentUpdateButton(commentId) - bottomMenuDialog.addCommentDeleteButton(commentId) - } else { - bottomMenuDialog.addCommentReportButton(commentId) - } - bottomMenuDialog.show() - } - - private fun BottomMenuDialog.addCommentUpdateButton(commentId: Long) { - addMenuItemBelow(context.getString(R.string.all_update_button_label)) { - editComment(commentId) - } - } - - private fun BottomMenuDialog.addCommentDeleteButton(commentId: Long) { - addMenuItemBelow(context.getString(R.string.all_delete_button_label)) { - onCommentDeleteButtonClick(commentId) + private fun observeFeedDetail() { + viewModel.feedDetailUiState.observe(this) { + val feedAndComments = listOf(it.feedUiState) + it.commentsUiState.commentUiStates + feedAndCommentsAdapter.submitList(feedAndComments) { + if (highlightCommentId == INVALID_COMMENT_ID || isNotRealFirstFetch()) return@submitList + viewModel.highlightComment(highlightCommentId) + viewModel.isAlreadyFirstFetched = true + } } } - private fun onCommentDeleteButtonClick(commentId: Long) { - WarningDialog( - context = this, - title = getString(R.string.commentdeletedialog_title), - message = getString(R.string.commentdeletedialog_message), - positiveButtonLabel = getString(R.string.commentdeletedialog_positive_button_label), - negativeButtonLabel = getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { deleteComment(commentId) }, - ).show() - } - - private fun BottomMenuDialog.addCommentReportButton(commentId: Long) { - addMenuItemBelow( - context.getString(R.string.all_report_button_label), - MenuItemType.IMPORTANT, - ) { reportComment(commentId) } - } - - private fun editComment(commentId: Long) { - viewModel.setEditMode(true, commentId) - binding.etCommentsCommentUpdate.requestFocus() - showKeyboard() - } - - private fun deleteComment(commentId: Long) { - viewModel.deleteComment(commentId) - } - - private fun reportComment(commentId: Long) { - val context = this - WarningDialog( - context = context, - title = context.getString(R.string.all_report_dialog_title), - message = context.getString(R.string.comments_comment_report_dialog_message), - positiveButtonLabel = context.getString(R.string.all_report_dialog_positive_button_label), - negativeButtonLabel = context.getString(R.string.commentdeletedialog_negative_button_label), - onPositiveButtonClick = { - viewModel.reportComment(commentId) - }, - ).show() - } - - private fun setUpCommentEditing() { - viewModel.editingCommentContent.observe(this) { - if (it == null) return@observe - binding.etCommentsCommentUpdate.setText(it) - } - } + private fun isNotRealFirstFetch(): Boolean = + viewModel.isAlreadyFirstFetched || viewModel.commentUiStates.isEmpty() - private fun setUpUiEvent() { + private fun observeUiEvent() { viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun handleUiEvent(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content) { - FeedDetailUiEvent.None -> {} - is FeedDetailUiEvent.UnexpectedError -> showToast(content.errorMessage) + private fun handleUiEvent(uiEvent: FeedDetailUiEvent) { + when (uiEvent) { FeedDetailUiEvent.CommentDeleteFail -> binding.root.showSnackBar(getString(R.string.comments_comments_delete_error_message)) FeedDetailUiEvent.CommentPostFail -> binding.root.showSnackBar(getString(R.string.comments_comments_posting_error_message)) FeedDetailUiEvent.CommentReportComplete -> InfoDialog( @@ -287,8 +253,9 @@ class FeedDetailActivity : AppCompatActivity() { title = getString(R.string.feeddetail_feed_delete_complete_title), message = getString(R.string.feeddetail_feed_delete_complete_message), buttonLabel = getString(R.string.all_okay), + onButtonClick = { finish() }, + cancelable = false, ).show() - finish() } FeedDetailUiEvent.FeedDeleteFail -> binding.root.showSnackBar(getString(R.string.feeddetail_feed_delete_fail_message)) @@ -298,29 +265,45 @@ class FeedDetailActivity : AppCompatActivity() { title = getString(R.string.feeddetail_deleted_feed_fetch_title), message = getString(R.string.feeddetail_deleted_feed_fetch_message), buttonLabel = getString(R.string.all_okay), + onButtonClick = { finish() }, + cancelable = false, ).show() - finish() } - FeedDetailUiEvent.CommentPostComplete -> scrollToLastPosition() + FeedDetailUiEvent.CommentPostComplete -> { + binding.btiwCommentPost.clearText() + scrollToLastPosition() + } + + is FeedDetailUiEvent.CommentHighlight -> highlightComment(uiEvent.commentId) } } private fun scrollToLastPosition() { - binding.rvFeeddetailFeedAndComments.smoothScrollToPosition(viewModel.feedDetail.value.comments.size + 1) + val commentsCount = viewModel.commentUiStates.size + binding.rvFeedAndComments.smoothScrollToPosition(commentsCount) } - private fun setUpFeedDetail() { - viewModel.feedDetail.observe(this) { - feedDetailAdapter.setFeedDetail(it) - commentsAdapter.submitList(it.comments) + private fun highlightComment(commentId: Long) { + val position = viewModel.commentUiStates + .indexOfFirst { + it.comment.id == commentId + } + + binding.rvFeedAndComments.scrollToPosition(position + 1) + lifecycleScope.launch { + delay(200L) + binding.rvFeedAndComments.layoutManager?.startSmoothScroll( + object : LinearSmoothScroller(this@FeedDetailActivity) { + override fun getVerticalSnapPreference(): Int = SNAP_TO_START + }.apply { targetPosition = position + 1 }, + ) } } companion object { private const val KEY_HIGHLIGHT_COMMENT_ID = "KEY_HIGHLIGHT_COMMENT_ID" private const val INVALID_COMMENT_ID: Long = -1 - private const val FEED_DETAIL_COUNT: Int = 1 fun startActivity( context: Context, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt index 78290d29d..b90a83c76 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/FeedDetailViewModel.kt @@ -3,26 +3,31 @@ package com.emmsale.presentation.ui.feedDetail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import com.emmsale.data.common.retrofit.callAdapter.Failure import com.emmsale.data.common.retrofit.callAdapter.NetworkError import com.emmsale.data.common.retrofit.callAdapter.Success import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Comment +import com.emmsale.data.model.Feed import com.emmsale.data.repository.interfaces.CommentRepository import com.emmsale.data.repository.interfaces.FeedRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.ScreenUiState import com.emmsale.presentation.common.firebase.analytics.logComment import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiEvent import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -33,7 +38,8 @@ class FeedDetailViewModel @Inject constructor( private val feedRepository: FeedRepository, private val commentRepository: CommentRepository, private val tokenRepository: TokenRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { + var isAlreadyFirstFetched: Boolean by Delegates.vetoable(false) { _, _, newValue -> newValue } @@ -42,207 +48,188 @@ class FeedDetailViewModel @Inject constructor( private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _feedDetail: NotNullMutableLiveData = - NotNullMutableLiveData(FeedDetailUiState.Loading) - val feedDetail: NotNullLiveData = _feedDetail + private val _feedDetailUiState = NotNullMutableLiveData(FeedDetailUiState()) + val feedDetailUiState: NotNullLiveData = _feedDetailUiState + + private val feed: Feed + get() = _feedDetailUiState.value.feedUiState.feed + + val commentUiStates: List + get() = _feedDetailUiState.value.commentsUiState.commentUiStates val isFeedDetailWrittenByLoginUser: Boolean - get() = _feedDetail.value.feedDetail.writer.id == uid + get() = feed.writer.id == uid private val _editingCommentId = MutableLiveData() val editingCommentId: LiveData = _editingCommentId - val editingCommentContent = _editingCommentId.map { - _feedDetail.value.comments.find { commentUiState -> commentUiState.comment.id == it }?.comment?.content + val editingCommentContent: LiveData = _editingCommentId.map { commentId -> + if (commentId == null) null else commentUiStates.find { it.comment.id == commentId }?.comment?.content } - private val _uiEvent: NotNullMutableLiveData> = - NotNullMutableLiveData(UiEvent(FeedDetailUiEvent.None)) - val uiEvent: NotNullLiveData> = _uiEvent + private val _canSubmitComment = NotNullMutableLiveData(true) + val canSubmitComment: NotNullLiveData = _canSubmitComment + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent init { - refresh() + fetchFeedAndComments() } - override fun refresh() { - fetchFeedDetail() - fetchComments() - } + private fun fetchFeedAndComments(): Job = viewModelScope.launch { + _screenUiState.value = ScreenUiState.LOADING - private fun fetchFeedDetail() { - viewModelScope.launch { - when (val result = feedRepository.getFeed(feedId)) { - is Failure -> { - if (result.code == DELETED_FEED_FETCH_ERROR_CODE) { - _uiEvent.value = UiEvent(FeedDetailUiEvent.DeletedFeedFetch) - } else { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - } - } - - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> _feedDetail.value = _feedDetail.value.copy( - fetchResult = FetchResult.SUCCESS, - feedDetail = result.data, - ) - - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + val (feedResult, commentResult) = listOf( + async { feedRepository.getFeed(feedId) }, + async { commentRepository.getComments(feedId) }, + ).awaitAll() + + when { + feedResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(feedResult.error?.message.toString()) } - } - } - private fun fetchComments() { - viewModelScope.launch { - when (val result = commentRepository.getComments(feedId)) { - is Failure -> {} - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> _feedDetail.value = _feedDetail.value.copy( - comments = result.data.flatMap { parentComment -> - listOf(CommentUiState.create(uid, parentComment)) + - parentComment.childComments.map { CommentUiState.create(uid, it) } - }, - ) - - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + commentResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(commentResult.error?.message.toString()) } - } - } - fun deleteFeed() { - viewModelScope.launch { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.LOADING) - when (val result = feedRepository.deleteFeed(feedId)) { - is Failure -> { - _uiEvent.value = UiEvent(FeedDetailUiEvent.FeedDeleteFail) - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.SUCCESS) - } + feedResult is Failure && feedResult.code == DELETED_FEED_FETCH_ERROR_CODE -> { + _uiEvent.value = FeedDetailUiEvent.DeletedFeedFetch + } - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) + feedResult is Failure || commentResult is Failure -> { + dispatchFetchFailEvent() + } - is Success -> - _uiEvent.value = UiEvent(FeedDetailUiEvent.FeedDeleteComplete) + feedResult is NetworkError || commentResult is NetworkError -> { + changeToNetworkErrorState() + return@launch + } - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + feedResult is Success && commentResult is Success -> { + val comments = commentResult.data as List + val feed = (feedResult.data as Feed).copy(commentCount = comments.undeletedCount()) + _feedDetailUiState.value = FeedDetailUiState(feed, comments, uid) } } + + _screenUiState.value = ScreenUiState.NONE } - fun saveComment(content: String) { - viewModelScope.launch { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.saveComment(content, feedId)) { - is Failure -> { - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentPostFail) - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.SUCCESS) - logComment(content, feedId) - } - - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> { - refresh() - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentPostComplete) - } - - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + override fun refresh(): Job = viewModelScope.launch { + val (feedResult, commentResult) = listOf( + async { feedRepository.getFeed(feedId) }, + async { commentRepository.getComments(feedId) }, + ).awaitAll() + + when { + feedResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(feedResult.error?.message.toString()) } - } - } - fun updateComment(commentId: Long, content: String) { - viewModelScope.launch { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.updateComment(commentId, content)) { - is Failure -> { - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentUpdateFail) - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.SUCCESS) - } - - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> { - _editingCommentId.value = null - refresh() - } - - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + commentResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(commentResult.error?.message.toString()) } - } - } - fun deleteComment(commentId: Long) { - viewModelScope.launch { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.LOADING) - when (val result = commentRepository.deleteComment(commentId)) { - is Failure -> { - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentDeleteFail) - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.SUCCESS) - } - - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> refresh() - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) + feedResult is Failure && feedResult.code == DELETED_FEED_FETCH_ERROR_CODE -> { + _uiEvent.value = FeedDetailUiEvent.DeletedFeedFetch + } + + feedResult is Failure || commentResult is Failure -> { + dispatchFetchFailEvent() + } + + feedResult is NetworkError || commentResult is NetworkError -> { + dispatchNetworkErrorEvent() + return@launch + } + + feedResult is Success && commentResult is Success -> { + val comments = commentResult.data as List + val feed = (feedResult.data as Feed).copy(commentCount = comments.undeletedCount()) + _feedDetailUiState.value = FeedDetailUiState(feed, comments, uid) } } + _screenUiState.value = ScreenUiState.NONE } - fun setEditMode(isEditMode: Boolean, commentId: Long = -1) { - if (!isEditMode) { - _editingCommentId.value = null - return + private fun List.undeletedCount(): Int = commentsCount() - deletedCommentsCount() + + private fun List.commentsCount(): Int = this.sumOf { it.childComments.size + 1 } + + private fun List.deletedCommentsCount(): Int = + this.sumOf { parentComment -> + parentComment.childComments.count { comment -> comment.isDeleted } + if (parentComment.isDeleted) 1 else 0 } + + fun deleteFeed(): Job = command( + command = { feedRepository.deleteFeed(feedId) }, + onSuccess = { _uiEvent.value = FeedDetailUiEvent.FeedDeleteComplete }, + onFailure = { _, _ -> _uiEvent.value = FeedDetailUiEvent.FeedDeleteFail }, + ) + + fun postComment(content: String): Job = commandAndRefresh( + command = { commentRepository.saveComment(content, feedId) }, + onSuccess = { _uiEvent.value = FeedDetailUiEvent.CommentPostComplete }, + onFailure = { _, _ -> + _uiEvent.value = FeedDetailUiEvent.CommentPostFail + logComment(content, feedId) + }, + onStart = { _canSubmitComment.value = false }, + onFinish = { _canSubmitComment.value = true }, + ) + + fun updateComment(commentId: Long, content: String): Job = commandAndRefresh( + command = { commentRepository.updateComment(commentId, content) }, + onSuccess = { _editingCommentId.value = null }, + onFailure = { _, _ -> _uiEvent.value = FeedDetailUiEvent.CommentUpdateFail }, + onStart = { _canSubmitComment.value = false }, + onFinish = { _canSubmitComment.value = true }, + ) + + fun deleteComment(commentId: Long): Job = commandAndRefresh( + command = { commentRepository.deleteComment(commentId) }, + onFailure = { _, _ -> _uiEvent.value = FeedDetailUiEvent.CommentDeleteFail }, + ) + + fun startEditComment(commentId: Long) { _editingCommentId.value = commentId + highlightComment(commentId) } - fun reportComment(commentId: Long) { - viewModelScope.launch { - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.LOADING) - val authorId = - _feedDetail.value.comments.find { it.comment.id == commentId }?.comment?.writer?.id - ?: return@launch - when (val result = commentRepository.reportComment(commentId, authorId, uid)) { - is Failure -> { - if (result.code == REPORT_DUPLICATE_ERROR_CODE) { - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentReportDuplicate) - } else { - _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentReportFail) - } - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.SUCCESS) - } - - NetworkError -> - _feedDetail.value = _feedDetail.value.copy(fetchResult = FetchResult.ERROR) - - is Success -> _uiEvent.value = UiEvent(FeedDetailUiEvent.CommentReportComplete) - is Unexpected -> - _uiEvent.value = - UiEvent(FeedDetailUiEvent.UnexpectedError(result.error.toString())) - } - } + fun cancelEditComment() { + _editingCommentId.value = null + unhighlightComment() } + fun reportComment(commentId: Long): Job = command( + command = { + val authorId = commentUiStates.find { it.comment.id == commentId } + ?.comment?.writer?.id + ?: throw IllegalArgumentException("화면에 없는 댓글을 지우려고 시도했습니다. 지우려는 댓글 아이디: $commentId") + commentRepository.reportComment(commentId, authorId, uid) + }, + onSuccess = { _uiEvent.value = FeedDetailUiEvent.CommentReportComplete }, + onFailure = { code, _ -> + if (code == REPORT_DUPLICATE_ERROR_CODE) { + _uiEvent.value = FeedDetailUiEvent.CommentReportDuplicate + } else { + _uiEvent.value = FeedDetailUiEvent.CommentReportFail + } + }, + ) + fun highlightComment(commentId: Long) { - _feedDetail.value = _feedDetail.value.highlightComment(commentId) + _feedDetailUiState.value = _feedDetailUiState.value.highlightComment(commentId) + _uiEvent.value = FeedDetailUiEvent.CommentHighlight(commentId) + } + + private fun unhighlightComment() { + _feedDetailUiState.value = _feedDetailUiState.value.unhighlightComment() } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentDiffUtil.kt new file mode 100644 index 000000000..fe8a76f0b --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentDiffUtil.kt @@ -0,0 +1,16 @@ +package com.emmsale.presentation.ui.feedDetail.recyclerView + +import androidx.recyclerview.widget.DiffUtil +import com.emmsale.presentation.ui.feedDetail.uiState.FeedOrCommentUiState + +object FeedAndCommentDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: FeedOrCommentUiState, + newItem: FeedOrCommentUiState, + ): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: FeedOrCommentUiState, + newItem: FeedOrCommentUiState, + ): Boolean = oldItem == newItem +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentsAdapter.kt new file mode 100644 index 000000000..cd136de54 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedAndCommentsAdapter.kt @@ -0,0 +1,41 @@ +package com.emmsale.presentation.ui.feedDetail.recyclerView + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.emmsale.data.model.Comment +import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.CommentViewHolder +import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.FeedDetailViewHolder +import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.FeedOrCommentViewHolder +import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState +import com.emmsale.presentation.ui.feedDetail.uiState.FeedOrCommentUiState +import com.emmsale.presentation.ui.feedDetail.uiState.FeedUiState + +class FeedAndCommentsAdapter( + private val onAuthorImageClick: (authorId: Long) -> Unit, + private val onCommentClick: (comment: Comment) -> Unit, + private val onCommentMenuClick: (isWrittenByLoginUser: Boolean, comment: Comment) -> Unit, +) : ListAdapter(FeedAndCommentDiffUtil) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedOrCommentViewHolder = + when (viewType) { + FeedUiState.VIEW_TYPE -> FeedDetailViewHolder( + parent = parent, + onAuthorImageClick = onAuthorImageClick, + ) + + CommentUiState.VIEW_TYPE -> CommentViewHolder( + parent = parent, + onCommentClick = onCommentClick, + onAuthorImageClick = onAuthorImageClick, + onCommentMenuClick = onCommentMenuClick, + ) + + else -> throw IllegalArgumentException("피드 혹은 댓글 뷰 홀더 타입이어야 합니다.") + } + + override fun onBindViewHolder(holder: FeedOrCommentViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun getItemViewType(position: Int): Int = getItem(position).viewType +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailAdapter.kt deleted file mode 100644 index 4aacd98e3..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/FeedDetailAdapter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.emmsale.presentation.ui.feedDetail.recyclerView - -import android.annotation.SuppressLint -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder.FeedDetailViewHolder -import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiState - -class FeedDetailAdapter( - private val onProfileImageClick: (authorId: Long) -> Unit, -) : RecyclerView.Adapter() { - private val items: MutableList = mutableListOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedDetailViewHolder = - FeedDetailViewHolder.from(parent, onProfileImageClick) - - override fun getItemCount(): Int = items.size - - override fun onBindViewHolder(holder: FeedDetailViewHolder, position: Int) { - holder.bind(items[0]) - } - - @SuppressLint("NotifyDataSetChanged") - fun setFeedDetail(feedDetailUiState: FeedDetailUiState) { - items.clear() - items.add(feedDetailUiState) - notifyDataSetChanged() - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt index 9bf260d3b..7eefc2a88 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/CommentViewHolder.kt @@ -2,18 +2,18 @@ package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import com.emmsale.R import com.emmsale.data.model.Comment import com.emmsale.databinding.ItemAllCommentBinding import com.emmsale.presentation.ui.feedDetail.uiState.CommentUiState +import com.emmsale.presentation.ui.feedDetail.uiState.FeedOrCommentUiState class CommentViewHolder( parent: ViewGroup, onCommentClick: (comment: Comment) -> Unit, onAuthorImageClick: (authorId: Long) -> Unit, - onCommentMenuClick: (isWrittenByLoginUser: Boolean, commentId: Long) -> Unit, -) : RecyclerView.ViewHolder( + onCommentMenuClick: (isWrittenByLoginUser: Boolean, comment: Comment) -> Unit, +) : FeedOrCommentViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_all_comment, parent, false), ) { private val binding = ItemAllCommentBinding.bind(itemView) @@ -24,7 +24,8 @@ class CommentViewHolder( binding.onCommentMenuClick = onCommentMenuClick } - fun bind(commentUiState: CommentUiState) { - binding.uiState = commentUiState + override fun bind(uiState: FeedOrCommentUiState) { + if (uiState !is CommentUiState) return + binding.uiState = uiState } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt index 1b70d140f..f572dd729 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedDetailViewHolder.kt @@ -2,22 +2,26 @@ package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView +import com.emmsale.R import com.emmsale.databinding.ItemFeeddetailFeedDetailBinding import com.emmsale.presentation.common.extension.dp import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration import com.emmsale.presentation.ui.feedDetail.recyclerView.FeedDetailImagesAdapter -import com.emmsale.presentation.ui.feedDetail.uiState.FeedDetailUiState +import com.emmsale.presentation.ui.feedDetail.uiState.FeedOrCommentUiState +import com.emmsale.presentation.ui.feedDetail.uiState.FeedUiState class FeedDetailViewHolder( - private val binding: ItemFeeddetailFeedDetailBinding, - onProfileImageClick: (authorId: Long) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { + parent: ViewGroup, + onAuthorImageClick: (authorId: Long) -> Unit, +) : FeedOrCommentViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_feeddetail_feed_detail, parent, false), +) { + private val binding = ItemFeeddetailFeedDetailBinding.bind(itemView) private val imageUrlsAdapter: FeedDetailImagesAdapter = FeedDetailImagesAdapter() init { - binding.onProfileImageClick = onProfileImageClick + binding.onAuthorImageClick = onAuthorImageClick binding.rvFeeddetailFeedDetailImages.apply { adapter = imageUrlsAdapter itemAnimator = null @@ -25,21 +29,13 @@ class FeedDetailViewHolder( } } - fun bind(feedDetailUiState: FeedDetailUiState) { - binding.uiState = feedDetailUiState - imageUrlsAdapter.submitList(feedDetailUiState.feedDetail.imageUrls) + override fun bind(uiState: FeedOrCommentUiState) { + if (uiState !is FeedUiState) return + binding.feed = uiState.feed + imageUrlsAdapter.submitList(uiState.feed.imageUrls) } companion object { private val IMAGE_INTERVAL: Int = 10.dp - - fun from( - parent: ViewGroup, - onProfileImageClick: (authorId: Long) -> Unit, - ): FeedDetailViewHolder { - val binding = ItemFeeddetailFeedDetailBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - return FeedDetailViewHolder(binding, onProfileImageClick) - } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedOrCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedOrCommentViewHolder.kt new file mode 100644 index 000000000..244869c7e --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/recyclerView/viewHolder/FeedOrCommentViewHolder.kt @@ -0,0 +1,10 @@ +package com.emmsale.presentation.ui.feedDetail.recyclerView.viewHolder + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.emmsale.presentation.ui.feedDetail.uiState.FeedOrCommentUiState + +abstract class FeedOrCommentViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + abstract fun bind(uiState: FeedOrCommentUiState) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt index 2c29cd898..d51e2f19e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentUiState.kt @@ -6,7 +6,12 @@ data class CommentUiState( val isWrittenByLoginUser: Boolean, val isHighlight: Boolean, val comment: Comment, -) { +) : FeedOrCommentUiState { + + override val id: Long = comment.id + + override val viewType: Int = VIEW_TYPE + val isUpdated: Boolean = comment.createdAt != comment.updatedAt val childCommentsCount = comment.childComments.count { !it.isDeleted } @@ -16,6 +21,8 @@ data class CommentUiState( fun unhighlight() = copy(isHighlight = false) companion object { + const val VIEW_TYPE = 1 + fun create(uid: Long, comment: Comment, isHighlight: Boolean = false): CommentUiState = CommentUiState( isWrittenByLoginUser = uid == comment.writer.id, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentsUiState.kt new file mode 100644 index 000000000..320f4ba95 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/CommentsUiState.kt @@ -0,0 +1,33 @@ +package com.emmsale.presentation.ui.feedDetail.uiState + +import com.emmsale.data.model.Comment + +data class CommentsUiState(val commentUiStates: List = emptyList()) { + + constructor(uid: Long, comments: List) : this( + comments.flatMap { comment -> + listOf(CommentUiState.create(uid, comment)) + + comment.childComments.map { childComment -> + CommentUiState.create(uid, childComment) + } + }, + ) + + constructor(uid: Long, comment: Comment) : this( + listOf(CommentUiState.create(uid, comment)) + + comment.childComments.map { childComment -> CommentUiState.create(uid, childComment) }, + ) + + val size: Int = commentUiStates.size + + operator fun get(commentId: Long): CommentUiState? = + commentUiStates.find { it.comment.id == commentId } + + fun highlight(commentId: Long) = copy( + commentUiStates = commentUiStates.map { if (it.comment.id == commentId) it.highlight() else it.unhighlight() }, + ) + + fun unhighlight() = copy( + commentUiStates = commentUiStates.map { it.unhighlight() }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt index 307b06d38..a25ff414b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiEvent.kt @@ -1,8 +1,6 @@ package com.emmsale.presentation.ui.feedDetail.uiState sealed interface FeedDetailUiEvent { - object None : FeedDetailUiEvent - data class UnexpectedError(val errorMessage: String) : FeedDetailUiEvent object DeletedFeedFetch : FeedDetailUiEvent object FeedDeleteFail : FeedDetailUiEvent object FeedDeleteComplete : FeedDetailUiEvent @@ -13,4 +11,5 @@ sealed interface FeedDetailUiEvent { object CommentReportFail : FeedDetailUiEvent object CommentReportComplete : FeedDetailUiEvent object CommentPostComplete : FeedDetailUiEvent + data class CommentHighlight(val commentId: Long) : FeedDetailUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt index 9f152d6cf..5d8beb5c9 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedDetailUiState.kt @@ -1,44 +1,20 @@ package com.emmsale.presentation.ui.feedDetail.uiState +import com.emmsale.data.model.Comment import com.emmsale.data.model.Feed -import com.emmsale.data.model.Member -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState -import java.time.LocalDateTime data class FeedDetailUiState( - override val fetchResult: FetchResult, - val feedDetail: Feed, - val comments: List, -) : FetchResultUiState() { - - val isUpdated: Boolean = feedDetail.createdAt != feedDetail.updatedAt - - val commentsCount: Int = comments.count { !it.comment.isDeleted } - - fun highlightComment(commentId: Long) = copy( - comments = comments.map { if (it.comment.id == commentId) it.highlight() else it }, + val feedUiState: FeedUiState = FeedUiState(), + val commentsUiState: CommentsUiState = CommentsUiState(), +) { + constructor(feed: Feed, comments: List, uid: Long) : this( + feedUiState = FeedUiState(feed), + commentsUiState = CommentsUiState(uid, comments), ) - fun unhighlightComment(commentId: Long) = copy( - comments = comments.map { if (it.comment.id == commentId) it.unhighlight() else it }, - ) + fun highlightComment(commentId: Long): FeedDetailUiState = + copy(commentsUiState = commentsUiState.highlight(commentId)) - companion object { - val Loading: FeedDetailUiState = FeedDetailUiState( - fetchResult = FetchResult.LOADING, - feedDetail = Feed( - id = 0, - eventId = 0, - title = "", - content = "", - writer = Member(), - imageUrls = emptyList(), - commentCount = 0, - createdAt = LocalDateTime.now(), - updatedAt = LocalDateTime.now(), - ), - comments = emptyList(), - ) - } + fun unhighlightComment(): FeedDetailUiState = + copy(commentsUiState = commentsUiState.unhighlight()) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedOrCommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedOrCommentUiState.kt new file mode 100644 index 000000000..1a12c1189 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedOrCommentUiState.kt @@ -0,0 +1,6 @@ +package com.emmsale.presentation.ui.feedDetail.uiState + +sealed interface FeedOrCommentUiState { + val id: Long + val viewType: Int +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedUiState.kt new file mode 100644 index 000000000..85bc58dea --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedDetail/uiState/FeedUiState.kt @@ -0,0 +1,16 @@ +package com.emmsale.presentation.ui.feedDetail.uiState + +import com.emmsale.data.model.Feed + +data class FeedUiState( + val feed: Feed = Feed(), +) : FeedOrCommentUiState { + + override val id: Long = feed.id + + override val viewType: Int = VIEW_TYPE + + companion object { + const val VIEW_TYPE = 0 + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListFragment.kt index 9dbe7e2b6..22d3956b9 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListFragment.kt @@ -5,41 +5,49 @@ import android.view.View import androidx.fragment.app.viewModels import com.emmsale.R import com.emmsale.databinding.FragmentFeedListBinding -import com.emmsale.presentation.base.BaseFragment +import com.emmsale.presentation.base.NetworkFragment +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity import com.emmsale.presentation.ui.feedList.FeedListViewModel.Companion.EVENT_ID_KEY import com.emmsale.presentation.ui.feedList.recyclerView.FeedListAdapter import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class FeedListFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_feed_list - private val viewModel: FeedListViewModel by viewModels() +class FeedListFragment : NetworkFragment(R.layout.fragment_feed_list) { + + override val viewModel: FeedListViewModel by viewModels() private val feedListAdapter: FeedListAdapter by lazy { FeedListAdapter(navigateToFeedDetail = ::navigateToFeedDetail) } + private fun navigateToFeedDetail(feedId: Long) { + FeedDetailActivity.startActivity(requireContext(), feedId) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.vm = viewModel - binding.rvFeedList.adapter = feedListAdapter - setUpFeeds() + setupFeedsRecyclerView() + + observeFeeds() } - override fun onResume() { - super.onResume() - viewModel.refresh() + private fun setupFeedsRecyclerView() { + binding.rvFeedList.adapter = feedListAdapter + binding.rvFeedList.addItemDecoration(DividerItemDecoration(requireContext())) } - private fun setUpFeeds() { + private fun observeFeeds() { viewModel.feeds.observe(viewLifecycleOwner) { - feedListAdapter.submitList(it.feeds) + feedListAdapter.submitList(it) } } - private fun navigateToFeedDetail(feedId: Long) { - FeedDetailActivity.startActivity(requireContext(), feedId) + override fun onResume() { + super.onResume() + viewModel.refresh() } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListViewModel.kt index 242b299cb..4030c4188 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/FeedListViewModel.kt @@ -1,50 +1,38 @@ package com.emmsale.presentation.ui.feedList import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Success +import com.emmsale.data.model.Feed import com.emmsale.data.repository.interfaces.FeedRepository -import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.feedList.uiState.FeedsUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class FeedListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val feedRepository: FeedRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { val eventId: Long = savedStateHandle[EVENT_ID_KEY] ?: DEFAULT_EVENT_ID - private val _feeds = NotNullMutableLiveData(FeedsUiState()) - val feeds: NotNullLiveData = _feeds + private val _feeds = NotNullMutableLiveData(listOf()) + val feeds: NotNullLiveData> = _feeds init { - refresh() - } - - override fun refresh() { fetchFeeds() } - private fun fetchFeeds() { - _feeds.value = feeds.value.copy(fetchResult = FetchResult.LOADING) - viewModelScope.launch { - when (val result = feedRepository.getFeeds(eventId)) { - is Success -> _feeds.value = feeds.value.copy( - fetchResult = FetchResult.SUCCESS, - feeds = result.data, - ) + private fun fetchFeeds(): Job = fetchData( + fetchData = { feedRepository.getFeeds(eventId) }, + onSuccess = { _feeds.value = it }, + ) - else -> _feeds.value = feeds.value.copy(fetchResult = FetchResult.ERROR) - } - } - } + override fun refresh(): Job = refreshData( + refresh = { feedRepository.getFeeds(eventId) }, + onSuccess = { _feeds.value = it }, + ) companion object { const val EVENT_ID_KEY = "EVENT_ID_KEY" diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/uiState/FeedsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/uiState/FeedsUiState.kt deleted file mode 100644 index 0e657d52a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedList/uiState/FeedsUiState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.emmsale.presentation.ui.feedList.uiState - -import com.emmsale.data.model.Feed -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class FeedsUiState( - override val fetchResult: FetchResult = FetchResult.LOADING, - val feeds: List = emptyList(), -) : FetchResultUiState() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingActivity.kt index 4ddc1e1d9..30cfcefbe 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingActivity.kt @@ -5,33 +5,37 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.MediaStore +import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityFeedWritingBinding -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.dp import com.emmsale.presentation.common.extension.navigateToApplicationDetailSetting import com.emmsale.presentation.common.extension.showPermissionRequestDialog +import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.imageUtil.getImageFileFromUri import com.emmsale.presentation.common.imageUtil.isImagePermissionGrantedCompat import com.emmsale.presentation.common.imageUtil.onImagePermissionCompat +import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration +import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity import com.emmsale.presentation.ui.feedWriting.FeedWritingViewModel.Companion.EVENT_ID_KEY import com.emmsale.presentation.ui.feedWriting.recyclerView.FeedWritingImageAdapter -import com.emmsale.presentation.ui.feedWriting.uiState.FeedUploadResultUiEvent +import com.emmsale.presentation.ui.feedWriting.uiState.FeedWritingUiEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class FeedWritingActivity : AppCompatActivity() { - private val binding by lazy { ActivityFeedWritingBinding.inflate(layoutInflater) } - private val viewModel: FeedWritingViewModel by viewModels() +class FeedWritingActivity : + NetworkActivity(R.layout.activity_feed_writing) { - private val adapter: FeedWritingImageAdapter by lazy { - FeedWritingImageAdapter(deleteImage = viewModel::deleteImageUrl) + override val viewModel: FeedWritingViewModel by viewModels() + + private val imagesAdapter: FeedWritingImageAdapter by lazy { + FeedWritingImageAdapter(deleteImage = viewModel::deleteImageUri) } private val imagePermissionLauncher = registerForActivityResult( @@ -56,77 +60,15 @@ class FeedWritingActivity : AppCompatActivity() { imageUris.isEmpty() -> Unit - else -> viewModel.fetchImageUris(imageUrls = imageUris) + else -> viewModel.setImageUris(imageUris = imageUris) } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpBinding() - setUpPostUploadResult() - setUpImageUrls() - setUpRegisterButtonClick() - setUpBackButtonClick() - } - private fun getImageUriPaths(intent: Intent): List { val clipData = intent.clipData ?: return listOf(intent.data.toString()) return (0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it).uri.toString() } } - private fun setUpBinding() { - setContentView(binding.root) - binding.rvFeedWritingImageList.adapter = adapter - binding.vm = viewModel - binding.showAlbum = ::showAlbum - binding.lifecycleOwner = this - } - - private fun setUpPostUploadResult() { - viewModel.feedUploadResult.observe(this, ::handleUploadPostResult) - } - - private fun handleUploadPostResult(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content.fetchResult) { - FetchResult.SUCCESS -> { - FeedDetailActivity.startActivity(this, content.responseId!!) - finish() - } - - FetchResult.ERROR -> showToast(getString(R.string.post_writing_upload_error)) - - FetchResult.LOADING -> Unit - } - } - - private fun setUpImageUrls() { - viewModel.imageUris.observe(this) { imageUrls -> - adapter.submitList(imageUrls) - } - } - - private fun setUpRegisterButtonClick() { - binding.tbToolbar.setOnMenuItemClickListener { - uploadPost() - true - } - } - - private fun uploadPost() { - when { - !viewModel.isTitleValid() -> showToast(getString(R.string.post_writing_title_warning)) - - !viewModel.isContentValid() -> showToast(getString(R.string.post_writing_content_warning)) - - else -> { - val imageUris = viewModel.imageUris.value - val imageFiles = imageUris.map { getImageFileFromUri(this, Uri.parse(it)) } - viewModel.uploadPost(imageFiles) - } - } - } - private fun showAlbum() { onImagePermissionCompat( onGranted = ::navigateToAlbum, @@ -156,9 +98,87 @@ class FeedWritingActivity : AppCompatActivity() { ) } - private fun setUpBackButtonClick() { - binding.tbToolbar.setNavigationOnClickListener { - finish() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + setupImagesAdapter() + + observeCanSubmit() + observeImageUrls() + observeUiEvent() + } + + private fun setupDataBinding() { + setContentView(binding.root) + binding.vm = viewModel + binding.showAlbum = ::showAlbum + } + + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModel.isChanged()) showFinishConfirmDialog() else finish() + } + }, + ) + } + + private fun showFinishConfirmDialog() { + WarningDialog( + context = this, + title = getString(R.string.recruitmentpostwriting_writing_cancel_confirm_dialog_title), + message = getString(R.string.recruitmentpostwriting_writing_cancel_confirm_dialog_message), + positiveButtonLabel = getString(R.string.all_okay), + negativeButtonLabel = getString(R.string.all_cancel), + onPositiveButtonClick = { finish() }, + ).show() + } + + private fun setupToolbar() { + binding.tbToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + + binding.tbToolbar.setOnMenuItemClickListener { + val imageUris = viewModel.imageUris.value + val imageFiles = imageUris.map { getImageFileFromUri(this, Uri.parse(it)) } + viewModel.uploadPost(imageFiles) + true + } + } + + private fun setupImagesAdapter() { + binding.rvFeedWritingImageList.adapter = imagesAdapter + binding.rvFeedWritingImageList.addItemDecoration(IntervalItemDecoration(width = 10.dp)) + } + + private fun observeCanSubmit() { + viewModel.calSubmit.observe(this) { canSubmit -> + binding.tbToolbar.menu.findItem(R.id.register).isEnabled = canSubmit + } + } + + private fun observeImageUrls() { + viewModel.imageUris.observe(this) { imageUrls -> + imagesAdapter.submitList(imageUrls) + } + } + + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: FeedWritingUiEvent) { + when (uiEvent) { + is FeedWritingUiEvent.PostComplete -> { + FeedDetailActivity.startActivity(this, uiEvent.feedId) + finish() + } + + FeedWritingUiEvent.PostFail -> binding.root.showSnackBar(R.string.post_writing_upload_error) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingViewModel.kt index 71bd6243c..1bbec533a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/FeedWritingViewModel.kt @@ -1,19 +1,17 @@ package com.emmsale.presentation.ui.feedWriting import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Success import com.emmsale.data.repository.interfaces.FeedRepository -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.ui.feedWriting.uiState.FeedUploadResultUiEvent +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.feedWriting.uiState.FeedWritingUiEvent import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import java.io.File import javax.inject.Inject @@ -21,66 +19,63 @@ import javax.inject.Inject class FeedWritingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val feedRepository: FeedRepository, -) : ViewModel() { +) : NetworkViewModel() { private val eventId = savedStateHandle[EVENT_ID_KEY] ?: DEFAULT_ID - private val _imageUris: NotNullMutableLiveData> = - NotNullMutableLiveData(emptyList()) - val imageUris: NotNullLiveData> = _imageUris - - private val _feedUploadResult: MutableLiveData> = - MutableLiveData() - val feedUploadResult: LiveData> = _feedUploadResult - val title = MutableLiveData() + private val titleIsNotBlank: Boolean + get() = title.value?.isNotBlank() ?: false + val content = MutableLiveData() + private val contentIsNotBlank: Boolean + get() = content.value?.isNotBlank() ?: false - fun uploadPost(imageFiles: List) { - _feedUploadResult.value = UiEvent(FeedUploadResultUiEvent(FetchResult.LOADING)) - viewModelScope.launch { - when ( - val fetchResult = - feedRepository.uploadFeed( - eventId, - title.value ?: DEFAULT_TITLE, - content.value ?: DEFAULT_CONTENT, - imageFiles, - ) - ) { - is Success -> - _feedUploadResult.value = - UiEvent(FeedUploadResultUiEvent(FetchResult.SUCCESS, fetchResult.data)) + private val _imageUris = NotNullMutableLiveData(emptyList()) + val imageUris: NotNullLiveData> = _imageUris - else -> - _feedUploadResult.value = - UiEvent(FeedUploadResultUiEvent(FetchResult.ERROR)) - } - } + private val _canSubmit = NotNullMutableLiveData(true) + val calSubmit = MediatorLiveData(false).apply { + addSource(title) { value = canSubmit() } + addSource(content) { value = canSubmit() } + addSource(_canSubmit) { value = canSubmit() } } - fun isTitleValid(): Boolean { - return (title.value?.length ?: 0) >= MINIMUM_TITLE_LENGTH - } + private fun canSubmit(): Boolean = titleIsNotBlank && contentIsNotBlank && _canSubmit.value - fun isContentValid(): Boolean { - return (content.value?.length ?: 0) >= MINIMUM_CONTENT_LENGTH - } + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - fun fetchImageUris(imageUrls: List) { - _imageUris.value = imageUrls + fun uploadPost(imageFiles: List): Job = command( + command = { + feedRepository.uploadFeed( + eventId, + title.value ?: DEFAULT_TITLE, + content.value ?: DEFAULT_CONTENT, + imageFiles, + ) + }, + onSuccess = { _uiEvent.value = FeedWritingUiEvent.PostComplete(it) }, + onFailure = { _, _ -> _uiEvent.value = FeedWritingUiEvent.PostFail }, + onLoading = { changeToLoadingState() }, + onStart = { _canSubmit.value = false }, + onFinish = { _canSubmit.value = true }, + ) + + fun setImageUris(imageUris: List) { + _imageUris.value = imageUris } - fun deleteImageUrl(imageUrl: String) { - val newUrls = _imageUris.value.toMutableList().apply { remove(imageUrl) } - _imageUris.value = newUrls + fun deleteImageUri(imageUri: String) { + _imageUris.value = _imageUris.value.filter { it != imageUri } } + fun isChanged(): Boolean = + titleIsNotBlank || contentIsNotBlank || _imageUris.value.isNotEmpty() + companion object { const val EVENT_ID_KEY = "EVENT_ID_KEY" private const val DEFAULT_TITLE = "" private const val DEFAULT_CONTENT = "" private const val DEFAULT_ID = -1L - private const val MINIMUM_TITLE_LENGTH = 1 - private const val MINIMUM_CONTENT_LENGTH = 8 } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/recyclerView/PostWritingImageViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/recyclerView/PostWritingImageViewHolder.kt index ec67258ef..b7ac4ea9d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/recyclerView/PostWritingImageViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/recyclerView/PostWritingImageViewHolder.kt @@ -12,7 +12,7 @@ class PostWritingImageViewHolder( fun bind(imageUrl: String) { binding.imageUrl = imageUrl - binding.deleteImage = deleteImage + binding.onImageDeleteButtonClick = deleteImage } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedUploadResultUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedUploadResultUiEvent.kt deleted file mode 100644 index 9dd093dae..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedUploadResultUiEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.emmsale.presentation.ui.feedWriting.uiState - -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class FeedUploadResultUiEvent( - override val fetchResult: FetchResult, - val responseId: Long? = null, -) : FetchResultUiState() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedWritingUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedWritingUiEvent.kt new file mode 100644 index 000000000..a15038946 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/feedWriting/uiState/FeedWritingUiEvent.kt @@ -0,0 +1,6 @@ +package com.emmsale.presentation.ui.feedWriting.uiState + +sealed interface FeedWritingUiEvent { + data class PostComplete(val feedId: Long) : FeedWritingUiEvent + object PostFail : FeedWritingUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt index 98745aeab..70efc377b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginActivity.kt @@ -6,20 +6,19 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.browser.customtabs.CustomTabsIntent import com.emmsale.BuildConfig import com.emmsale.R import com.emmsale.databinding.ActivityLoginBinding +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.checkPostNotificationPermission import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl -import com.emmsale.presentation.ui.login.uiState.LoginUiState +import com.emmsale.presentation.ui.login.uiState.LoginUiEvent import com.emmsale.presentation.ui.main.MainActivity import com.emmsale.presentation.ui.onboarding.OnboardingActivity import com.google.firebase.messaging.FirebaseMessaging @@ -27,10 +26,10 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class LoginActivity : - AppCompatActivity(), + NetworkActivity(R.layout.activity_login), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("login") { - private val viewModel: LoginViewModel by viewModels() - private val binding by lazy { ActivityLoginBinding.inflate(layoutInflater) } + + override val viewModel: LoginViewModel by viewModels() private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -41,70 +40,61 @@ class LoginActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - registerScreen(this) - binding.viewModel = viewModel - setupClickListener() - setupLoginState() - askNotificationPermission() - } - private fun setupClickListener() { - setupGithubLoginClickListener() - } + registerScreen(this) - private fun setupGithubLoginClickListener() { - binding.btnGithubLogin.setOnClickListener { - navigateToGithubLogin() - } - } + setupDataBinding() - private fun setupLoginState() { - viewModel.loginState.observe(this) { loginState -> - when (loginState) { - is LoginUiState.Login -> navigateToMain() - is LoginUiState.Onboarded -> navigateToOnboarding() - is LoginUiState.Loading -> changeLoadingVisibility(true) - is LoginUiState.Error -> showLoginFailedMessage() - } - } - } + observeUiEvent() - private fun navigateToMain() { - MainActivity.startActivity(this) - finish() + askNotificationPermission() } - private fun navigateToOnboarding() { - OnboardingActivity.startActivity(this) - finish() + private fun setupDataBinding() { + binding.viewModel = viewModel + binding.onLoginButtonClick = ::navigateToGithubLogin } private fun navigateToGithubLogin() { val customTabIntent = CustomTabsIntent.Builder().build() - customTabIntent.launchUrl(this, getGithubLoginUri()) + customTabIntent.launchUrl(this, createGithubLoginUri()) } - private fun getGithubLoginUri(): Uri = uri { - scheme("https") - authority("github.com") - appendPath("login") - appendPath("oauth") - appendPath("authorize") - appendQueryParameter("client_id", BuildConfig.GITHUB_CLIENT_ID) + private fun createGithubLoginUri() = Uri.Builder() + .scheme("https") + .authority("github.com") + .appendPath("login") + .appendPath("oauth") + .appendPath("authorize") + .appendQueryParameter("client_id", BuildConfig.GITHUB_CLIENT_ID) + .build() + + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun showLoginFailedMessage() { - changeLoadingVisibility(false) - binding.root.showSnackBar(getString(R.string.login_failed_message)) - } + private fun handleUiEvent(uiEvent: LoginUiEvent) { + when (uiEvent) { + LoginUiEvent.JoinComplete -> { + OnboardingActivity.startActivity(this) + finish() + } - private fun changeLoadingVisibility(isShow: Boolean) { - when (isShow) { - true -> binding.pbLogin.visibility = View.VISIBLE - false -> binding.pbLogin.visibility = View.GONE + LoginUiEvent.LoginComplete -> { + MainActivity.startActivity(this) + finish() + } + + LoginUiEvent.LoginFail -> binding.root.showSnackBar(getString(R.string.login_failed_message)) } } + @SuppressLint("InlinedApi") + private fun askNotificationPermission() { + if (checkPostNotificationPermission()) return + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) githubLogin(intent) @@ -119,8 +109,6 @@ class LoginActivity : } } - private fun uri(block: Uri.Builder.() -> Unit): Uri = Uri.Builder().apply(block).build() - private fun Intent.parseGithubCode(): String? = data?.getQueryParameter(GITHUB_CODE_PARAMETER) companion object { @@ -131,10 +119,4 @@ class LoginActivity : context.startActivity(intent) } } - - @SuppressLint("InlinedApi") - private fun askNotificationPermission() { - if (checkPostNotificationPermission()) return - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt index 244f8fd5c..185aab631 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/LoginViewModel.kt @@ -1,22 +1,19 @@ package com.emmsale.presentation.ui.login import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.model.Login +import com.emmsale.data.model.Token import com.emmsale.data.repository.interfaces.ConfigRepository import com.emmsale.data.repository.interfaces.FcmTokenRepository import com.emmsale.data.repository.interfaces.LoginRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.ui.login.uiState.LoginUiState +import com.emmsale.presentation.base.NetworkViewModel +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.login.uiState.LoginUiEvent import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import javax.inject.Inject @@ -26,42 +23,34 @@ class LoginViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val fcmTokenRepository: FcmTokenRepository, private val configRepository: ConfigRepository, -) : ViewModel() { - private val _loginState: MutableLiveData = MutableLiveData() - val loginState: LiveData = _loginState - - fun login(fcmToken: String, code: String) { - changeLoginState(LoginUiState.Loading) - - viewModelScope.launch { - when (val result = loginRepository.saveGithubCode(code)) { - is Failure, NetworkError -> changeLoginState(LoginUiState.Error) - is Success -> saveTokens(result.data, fcmToken) - is Unexpected -> throw Throwable(result.error) - } +) : NetworkViewModel() { + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent + + fun login(fcmToken: String, code: String): Job = command( + command = { loginRepository.saveGithubCode(code) }, + onSuccess = { + saveTokens(it.token, fcmToken) + handleLoginComplete(it) + }, + onFailure = { _, _ -> _uiEvent.value = LoginUiEvent.LoginFail }, + onLoading = { changeToLoadingState() }, + ) + + private fun saveTokens(token: Token, fcmToken: String) { + CoroutineScope(Dispatchers.Default).launch { + fcmTokenRepository.saveFcmToken(token.uid, fcmToken) } + tokenRepository.saveToken(token) } - private suspend fun saveTokens(login: Login, fcmToken: String) { - joinAll(saveUserToken(login), saveFcmToken(login.token.uid, fcmToken)) - + private fun handleLoginComplete(login: Login) { if (login.isDoneOnboarding) { - changeLoginState(LoginUiState.Login) + _uiEvent.value = LoginUiEvent.LoginComplete configRepository.saveAutoLoginConfig(true) } else { - changeLoginState(LoginUiState.Onboarded) + _uiEvent.value = LoginUiEvent.JoinComplete } } - - private fun saveFcmToken(uid: Long, fcmToken: String): Job = viewModelScope.launch { - fcmTokenRepository.saveFcmToken(uid, fcmToken) - } - - private fun saveUserToken(login: Login): Job = viewModelScope.launch { - tokenRepository.saveToken(login.token) - } - - private fun changeLoginState(loginState: LoginUiState) { - _loginState.postValue(loginState) - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiEvent.kt new file mode 100644 index 000000000..c664ca146 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiEvent.kt @@ -0,0 +1,7 @@ +package com.emmsale.presentation.ui.login.uiState + +sealed interface LoginUiEvent { + object LoginComplete : LoginUiEvent + object LoginFail : LoginUiEvent + object JoinComplete : LoginUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiState.kt deleted file mode 100644 index 301b19332..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/login/uiState/LoginUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.emmsale.presentation.ui.login.uiState - -sealed class LoginUiState { - object Login : LoginUiState() - object Onboarded : LoginUiState() - object Loading : LoginUiState() - object Error : LoginUiState() -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt index 1918ec74a..013c32e07 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListActivity.kt @@ -6,16 +6,15 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.emmsale.R import com.emmsale.databinding.ActivityMessageListBinding -import com.emmsale.presentation.common.EventObserver -import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.KeyboardHider +import com.emmsale.presentation.common.extension.showNotification import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.ui.messageList.MessageListViewModel.Companion.KEY_OTHER_UID import com.emmsale.presentation.ui.messageList.MessageListViewModel.Companion.KEY_ROOM_ID @@ -23,44 +22,49 @@ import com.emmsale.presentation.ui.messageList.recyclerview.MessageListAdapter import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent import com.emmsale.presentation.ui.profile.ProfileActivity import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @AndroidEntryPoint -class MessageListActivity : AppCompatActivity() { - private val binding by lazy { ActivityMessageListBinding.inflate(layoutInflater) } - private val viewModel: MessageListViewModel by viewModels() +class MessageListActivity : + NetworkActivity(R.layout.activity_message_list) { + + override val viewModel: MessageListViewModel by viewModels() private val keyboardHider by lazy { KeyboardHider(this) } private val messageListAdapter by lazy { MessageListAdapter(onProfileClick = ::navigateToProfile) } - private var job: Job? = null + private var bottomMessageShowingJob: Job? = null + + private fun navigateToProfile(uid: Long) { + ProfileActivity.startActivity(this, uid) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setupBinding() + setContentView(binding.root) + + setupDataBinding() setupToolbar() setupMessageRecyclerView() - setupMessages() - setUpEventUiEvent() + + observeMessages() + observeUiEvent() } - private fun setupBinding() { - setContentView(binding.root) + private fun setupDataBinding() { binding.vm = viewModel - binding.lifecycleOwner = this } private fun setupToolbar() { - binding.tbMessageList.setNavigationOnClickListener { - finish() - } + binding.tbMessageList.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } @SuppressLint("ClickableViewAccessibility") private fun setupMessageRecyclerView() { - binding.rvMessageList.setHasFixedSize(true) binding.rvMessageList.itemAnimator = null binding.rvMessageList.adapter = messageListAdapter binding.rvMessageList.setOnScrollChangeListener { v, _, _, _, _ -> @@ -73,68 +77,87 @@ class MessageListActivity : AppCompatActivity() { } } - private fun navigateToProfile(uid: Long) { - ProfileActivity.startActivity(this, uid) + private fun observeMessages() { + viewModel.messages.observe(this) { + messageListAdapter.submitList(it.messages) { + if (binding.rvMessageList.childCount == 0) return@submitList + if (!binding.rvMessageList.canScrollVertically(BOTTOM_SCROLL_DIRECTION)) smoothScrollToEnd() + } + } } - private fun setupMessages() { - viewModel.messages.observe(this) { uiState -> - if (uiState.fetchResult != FetchResult.SUCCESS) return@observe - messageListAdapter.submitList(uiState.messages) - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun scrollToEnd() { - val lastPosition = viewModel.messages.value.messageSize - 1 + private fun handleUiEvent(uiEvent: MessageListUiEvent) { + when (uiEvent) { + MessageListUiEvent.MessageSendComplete -> { + binding.btiwSendMessage.clearText() + smoothScrollToEnd() + } - // RecyclerView 버그로 scrollToPosition이 완전히 마지막으로 이동하지 않아서 아래와 같이 작성함. - binding.rvMessageList.scrollToPosition(lastPosition) - lifecycleScope.launch { - delay(50) - binding.rvMessageList.smoothScrollToPosition(lastPosition) + MessageListUiEvent.MessageSendFail -> binding.root.showSnackBar(R.string.messagelist_message_sent_failed) } } private fun smoothScrollToEnd() { - binding.rvMessageList.smoothScrollToPosition(viewModel.messages.value.messageSize) - } - - private fun setUpEventUiEvent() { - viewModel.uiEvent.observe(this, EventObserver(::handleEvent)) - } - - private fun handleEvent(event: MessageListUiEvent) { - when (event) { - MessageListUiEvent.MESSAGE_LIST_FIRST_LOADED -> scrollToEnd() - MessageListUiEvent.MESSAGE_SENDING -> binding.etMessageInput.text.clear() - MessageListUiEvent.MESSAGE_SENT_REFRESHED -> smoothScrollToEnd() - MessageListUiEvent.MESSAGE_SENT_FAILED -> binding.root.showSnackBar(R.string.messagelist_message_sent_failed) - MessageListUiEvent.NOT_FOUND_OTHER_MEMBER -> binding.root.showSnackBar(R.string.messagelist_not_found_other_member) - } + binding.rvMessageList.smoothScrollToPosition(viewModel.messages.value.size) } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - viewModel.refresh() - - val roomId = intent?.getStringExtra(KEY_ROOM_ID) - if (roomId != viewModel.roomId) return + val roomId = intent.getStringExtra(KEY_ROOM_ID) ?: return val profileUrl = intent.getStringExtra(KEY_PROFILE_URL) val otherName = intent.getStringExtra(KEY_OTHER_NAME) ?: return val messageContent = intent.getStringExtra(KEY_MESSAGE_CONTENT) ?: return + if (roomId != viewModel.roomId) { + notifyOtherRoomNewMessage( + otherRoomId = roomId, + profileUrl = profileUrl, + otherName = otherName, + messageContent = messageContent, + ) + return + } + + val couldNotScrollVertically = binding.rvMessageList + .canScrollVertically(BOTTOM_SCROLL_DIRECTION) + .not() + viewModel.refresh() + if (couldNotScrollVertically) return + showNewMessage(profileUrl, otherName, messageContent) } + private fun notifyOtherRoomNewMessage( + otherRoomId: String, + profileUrl: String?, + otherName: String, + messageContent: String, + ) { + CoroutineScope(Dispatchers.Default).launch { + showNotification( + title = otherName, + message = messageContent, + notificationId = otherRoomId.hashCode(), + channelId = R.id.id_all_message_notification_channel, + intent = intent, + largeIconUrl = profileUrl, + groupKey = otherRoomId, + ) + } + } + private fun showNewMessage(profileUrl: String?, otherName: String, messageContent: String) { val layoutManager = binding.rvMessageList.layoutManager as LinearLayoutManager val lastVisiblePos = layoutManager.findLastVisibleItemPosition() - val itemCount = viewModel.messages.value.messages.size - val lastPosition = itemCount - 1 + val lastPosition = viewModel.messages.value.size - if (lastVisiblePos != lastPosition) { - job?.cancel() - job = lifecycleScope.launch { + if (lastVisiblePos < lastPosition) { + bottomMessageShowingJob?.cancel() + bottomMessageShowingJob = lifecycleScope.launch { showBottomMessage(profileUrl, otherName, messageContent) delay(4000) hideBottomMessage() diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListViewModel.kt index 7b05fdb15..e494c69cc 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/MessageListViewModel.kt @@ -3,30 +3,28 @@ package com.emmsale.presentation.ui.messageList import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.emmsale.data.common.retrofit.callAdapter.Failure +import com.emmsale.data.common.retrofit.callAdapter.NetworkError import com.emmsale.data.common.retrofit.callAdapter.Success +import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.model.Member import com.emmsale.data.model.Message import com.emmsale.data.repository.interfaces.MemberRepository import com.emmsale.data.repository.interfaces.MessageRoomRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.ScreenUiState import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.messageList.uistate.MessageDateUiState +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent -import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent.MESSAGE_LIST_FIRST_LOADED -import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent.MESSAGE_SENDING -import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent.MESSAGE_SENT_FAILED -import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent.MESSAGE_SENT_REFRESHED -import com.emmsale.presentation.ui.messageList.uistate.MessageListUiEvent.NOT_FOUND_OTHER_MEMBER -import com.emmsale.presentation.ui.messageList.uistate.MessageUiState import com.emmsale.presentation.ui.messageList.uistate.MessagesUiState -import com.emmsale.presentation.ui.messageList.uistate.MyMessageUiState -import com.emmsale.presentation.ui.messageList.uistate.OtherMessageUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import javax.inject.Inject @@ -36,118 +34,71 @@ class MessageListViewModel @Inject constructor( tokenRepository: TokenRepository, private val memberRepository: MemberRepository, private val messageRoomRepository: MessageRoomRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { val roomId = savedStateHandle[KEY_ROOM_ID] ?: DEFAULT_ROOM_ID private val otherUid = savedStateHandle[KEY_OTHER_UID] ?: DEFAULT_OTHER_ID private val myUid: Long = tokenRepository.getMyUid() ?: -1 - private val _messages = NotNullMutableLiveData(MessagesUiState()) - val messages: NotNullLiveData = _messages - - private val _uiEvent = MutableLiveData>() - val uiEvent: LiveData> = _uiEvent - private val _otherMember = MutableLiveData() val otherMember: LiveData = _otherMember - init { - viewModelScope.launch { - fetchMessages() - _uiEvent.value = UiEvent(MESSAGE_LIST_FIRST_LOADED) - } - } - - override fun refresh() { - viewModelScope.launch { fetchMessages() } - } + private val _messages = NotNullMutableLiveData(MessagesUiState()) + val messages: NotNullLiveData = _messages - private suspend fun fetchMessages() { - loading() - fetchOtherMember() + private val _canSendMessage = NotNullMutableLiveData(true) + val canSendMessage: NotNullLiveData = _canSendMessage - when (val messagesResult = messageRoomRepository.getMessagesByRoomId(roomId, myUid)) { - is Success -> updateMessages(messagesResult.data) - else -> _messages.value = messages.value.toError() - } - } + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - private fun loading() { - _messages.value = messages.value.toLoading() + init { + changeToLoadingState() + refresh() } - private suspend fun fetchOtherMember() { - when (val otherMemberResult = memberRepository.getMember(otherUid)) { - is Success -> _otherMember.value = otherMemberResult.data - else -> _uiEvent.value = UiEvent(NOT_FOUND_OTHER_MEMBER) - } - } + override fun refresh(): Job = viewModelScope.launch { + val (otherMemberResult, messagesResult) = listOf( + async { memberRepository.getMember(otherUid) }, + async { messageRoomRepository.getMessagesByRoomId(roomId, myUid) }, + ).awaitAll() - private fun updateMessages(newMessages: List) { - val messagesNotBlank = newMessages - .filter { it.content.isNotBlank() } - .toUiState() - _messages.value = messages.value.toSuccess(messagesNotBlank) - } + when { + otherMemberResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(otherMemberResult.error?.message.toString()) + } - private fun List.toUiState(): List { - val newMessages = mutableListOf() + messagesResult is Unexpected -> { + _commonUiEvent.value = + CommonUiEvent.Unexpected(messagesResult.error?.message.toString()) + } - forEachIndexed { index, message -> - when { - index == 0 -> { - newMessages += message.createMessageDateUiState() - newMessages += message.createChatMessageUiState() - } + otherMemberResult is Failure || messagesResult is Failure -> dispatchFetchFailEvent() - message.isDifferentDate(this[index - 1]) -> { - newMessages += message.createMessageDateUiState() - } + otherMemberResult is NetworkError || messagesResult is NetworkError -> { + dispatchNetworkErrorEvent() + return@launch } - val previousMessage = getOrNull(index - 1) ?: return@forEachIndexed - val shouldShowProfile = message.shouldShowMemberProfile(previousMessage) - newMessages += message.createChatMessageUiState(shouldShowProfile) + otherMemberResult is Success && messagesResult is Success -> { + _otherMember.value = otherMemberResult.data as Member + _messages.value = + MessagesUiState.create(messagesResult.data as List, myUid) + } } - return newMessages + _screenUiState.value = ScreenUiState.NONE } - private fun Message.shouldShowMemberProfile(prevMessage: Message): Boolean { - return isSameDateTime(prevMessage) || - isDifferentSender(prevMessage) || - isDifferentDate(prevMessage) - } - - private fun Message.createMessageDateUiState(): MessageDateUiState = MessageDateUiState( - messageDate = createdAt, + fun sendMessage(message: String): Job = commandAndRefresh( + command = { messageRoomRepository.sendMessage(myUid, otherUid, message) }, + onSuccess = { _uiEvent.value = MessageListUiEvent.MessageSendComplete }, + onFailure = { _, _ -> _uiEvent.value = MessageListUiEvent.MessageSendFail }, + onStart = { _canSendMessage.value = false }, + onFinish = { _canSendMessage.value = true }, ) - private fun Message.createChatMessageUiState( - shouldShowProfile: Boolean = true, - ): MessageUiState = when (sender.id) { - myUid -> MyMessageUiState.create(this, shouldShowProfile) - else -> OtherMessageUiState.create(this, shouldShowProfile) - } - - fun sendMessage(message: String) { - if (message.isBlank()) return - - _uiEvent.value = UiEvent(MESSAGE_SENDING) - loading() - - viewModelScope.launch { - when (messageRoomRepository.sendMessage(myUid, otherUid, message)) { - is Success -> { - fetchMessages() - _uiEvent.value = UiEvent(MESSAGE_SENT_REFRESHED) - } - - else -> _uiEvent.value = UiEvent(MESSAGE_SENT_FAILED) - } - } - } - companion object { const val KEY_ROOM_ID = "KEY_ROOM_ID" private const val DEFAULT_ROOM_ID = "" diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessageListUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessageListUiEvent.kt index 6f1619524..98fd5927e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessageListUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessageListUiEvent.kt @@ -1,9 +1,6 @@ package com.emmsale.presentation.ui.messageList.uistate -enum class MessageListUiEvent { - MESSAGE_LIST_FIRST_LOADED, - MESSAGE_SENDING, - MESSAGE_SENT_FAILED, - MESSAGE_SENT_REFRESHED, - NOT_FOUND_OTHER_MEMBER, +sealed interface MessageListUiEvent { + object MessageSendComplete : MessageListUiEvent + object MessageSendFail : MessageListUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessagesUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessagesUiState.kt index fd194202e..f03a524d2 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessagesUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageList/uistate/MessagesUiState.kt @@ -1,22 +1,62 @@ package com.emmsale.presentation.ui.messageList.uistate -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState +import com.emmsale.data.model.Message data class MessagesUiState( - override val fetchResult: FetchResult = FetchResult.LOADING, val messages: List = emptyList(), -) : FetchResultUiState() { +) { - val messageSize: Int = messages.size + val size: Int = messages.size - fun toSuccess( - messages: List, - ): MessagesUiState = MessagesUiState( - fetchResult = FetchResult.SUCCESS, - messages = messages, - ) + fun isEmpty(): Boolean = messages.isEmpty() - fun toLoading(): MessagesUiState = copy(fetchResult = FetchResult.LOADING) - fun toError(): MessagesUiState = copy(fetchResult = FetchResult.ERROR) + companion object { + fun create(messages: List, myUid: Long): MessagesUiState { + val messagesNotBlank = messages + .filter { it.content.isNotBlank() } + .toUiState(myUid) + return MessagesUiState(messagesNotBlank) + } + + private fun List.toUiState(myUid: Long): List { + val newMessages = mutableListOf() + + forEachIndexed { index, message -> + when { + index == 0 -> { + newMessages += message.createMessageDateUiState() + newMessages += message.createChatMessageUiState(true, myUid) + } + + message.isDifferentDate(this[index - 1]) -> { + newMessages += message.createMessageDateUiState() + } + } + + val previousMessage = getOrNull(index - 1) ?: return@forEachIndexed + val shouldShowProfile = message.shouldShowMemberProfile(previousMessage) + newMessages += message.createChatMessageUiState(shouldShowProfile, myUid) + } + + return newMessages + } + + private fun Message.shouldShowMemberProfile(prevMessage: Message): Boolean { + return isSameDateTime(prevMessage) || + isDifferentSender(prevMessage) || + isDifferentDate(prevMessage) + } + + private fun Message.createMessageDateUiState(): MessageDateUiState = MessageDateUiState( + messageDate = createdAt, + ) + + private fun Message.createChatMessageUiState( + shouldShowProfile: Boolean = true, + myUid: Long, + ): MessageUiState = when (sender.id) { + myUid -> MyMessageUiState.create(this, shouldShowProfile) + else -> OtherMessageUiState.create(this, shouldShowProfile) + } + } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomFragment.kt index cf09707eb..5bdb5ed20 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomFragment.kt @@ -5,18 +5,22 @@ import android.view.View import androidx.fragment.app.viewModels import com.emmsale.R import com.emmsale.databinding.FragmentMessageRoomBinding -import com.emmsale.presentation.base.BaseFragment -import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.base.NetworkFragment import com.emmsale.presentation.ui.messageList.MessageListActivity import com.emmsale.presentation.ui.messageRoomList.recyclerview.MessageRoomListAdapter import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MessageRoomFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_message_room - private val viewModel: MessageRoomViewModel by viewModels() +class MessageRoomFragment : + NetworkFragment(R.layout.fragment_message_room) { - private lateinit var messageRoomListAdapter: MessageRoomListAdapter + override val viewModel: MessageRoomViewModel by viewModels() + + private val messageRoomListAdapter by lazy { MessageRoomListAdapter(::navigateToMessageList) } + + private fun navigateToMessageList(roomId: String, otherUid: Long) { + startActivity(MessageListActivity.getIntent(requireContext(), roomId, otherUid)) + } override fun onResume() { super.onResume() @@ -25,32 +29,27 @@ class MessageRoomFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupBinding() + + setupDataBinding() setupMessageRoomRecyclerView() - setupMessageRooms() + + observeMessageRooms() } - private fun setupBinding() { - binding.lifecycleOwner = this + private fun setupDataBinding() { binding.vm = viewModel } private fun setupMessageRoomRecyclerView() { - messageRoomListAdapter = MessageRoomListAdapter(::navigateToMessageList) binding.rvMessageRoomList.adapter = messageRoomListAdapter } - private fun setupMessageRooms() { - viewModel.messageRooms.observe(viewLifecycleOwner) { uiState -> - if (uiState.fetchResult != FetchResult.SUCCESS) return@observe - messageRoomListAdapter.submitList(uiState.messageRooms) + private fun observeMessageRooms() { + viewModel.messageRooms.observe(viewLifecycleOwner) { messageRooms -> + messageRoomListAdapter.submitList(messageRooms) } } - private fun navigateToMessageList(roomId: String, otherUid: Long) { - startActivity(MessageListActivity.getIntent(requireContext(), roomId, otherUid)) - } - companion object { const val TAG: String = "TAG_MESSAGE_ROOM" } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomViewModel.kt index f1bca949e..e758a06bb 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/MessageRoomViewModel.kt @@ -1,47 +1,35 @@ package com.emmsale.presentation.ui.messageRoomList -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.MessageRoom import com.emmsale.data.repository.interfaces.MessageRoomRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.messageRoomList.uistate.MessageRoomListUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class MessageRoomViewModel @Inject constructor( - private val memberRepository: TokenRepository, + private val tokenRepository: TokenRepository, private val messageRoomRepository: MessageRoomRepository, -) : ViewModel(), Refreshable { - private val _messageRooms = NotNullMutableLiveData(MessageRoomListUiState()) - val messageRooms: NotNullLiveData = _messageRooms +) : RefreshableViewModel() { - override fun refresh() { - fetchMessageRooms() - } - - private fun fetchMessageRooms() { - val uid = memberRepository.getMyUid() ?: return - _messageRooms.value = messageRooms.value.toLoading() + private val uid: Long by lazy { tokenRepository.getMyUid()!! } - viewModelScope.launch { - when (val result = messageRoomRepository.getMessageRooms(uid)) { - is Success -> { - _messageRooms.value = messageRooms.value.toSuccess(result.data) - } + private val _messageRooms = NotNullMutableLiveData(emptyList()) + val messageRooms: NotNullLiveData> = _messageRooms - is Failure, NetworkError, is Unexpected -> { - _messageRooms.value = messageRooms.value.toError() - } - } - } + init { + fetchData( + fetchData = { messageRoomRepository.getMessageRooms(uid) }, + onSuccess = { _messageRooms.value = it }, + ) } + + override fun refresh(): Job = refreshData( + refresh = { messageRoomRepository.getMessageRooms(uid) }, + onSuccess = { _messageRooms.value = it }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/uistate/MessageRoomListUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/uistate/MessageRoomListUiState.kt deleted file mode 100644 index d29fe883d..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/messageRoomList/uistate/MessageRoomListUiState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.emmsale.presentation.ui.messageRoomList.uistate - -import com.emmsale.data.model.MessageRoom -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class MessageRoomListUiState( - override val fetchResult: FetchResult = FetchResult.LOADING, - val messageRooms: List = emptyList(), -) : FetchResultUiState() { - fun toSuccess( - messageRooms: List, - ): MessageRoomListUiState = copy( - fetchResult = FetchResult.SUCCESS, - messageRooms = messageRooms, - ) - - fun toLoading() = copy(fetchResult = FetchResult.LOADING) - - fun toError() = copy(fetchResult = FetchResult.ERROR) -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt index 79f1aff47..a68e69f12 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsActivity.kt @@ -4,97 +4,69 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R +import com.emmsale.data.model.Comment import com.emmsale.databinding.ActivityMyCommentsBinding -import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.recyclerView.DividerItemDecoration -import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity -import com.emmsale.presentation.ui.login.LoginActivity +import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity import com.emmsale.presentation.ui.myCommentList.recyclerView.MyCommentsAdapter -import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentsUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MyCommentsActivity : AppCompatActivity() { - private val binding by lazy { ActivityMyCommentsBinding.inflate(layoutInflater) } +class MyCommentsActivity : + NetworkActivity(R.layout.activity_my_comments) { - private val viewModel: MyCommentsViewModel by viewModels() + override val viewModel: MyCommentsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - initDataBinding() - initToolbar() - initMyCommentsRecyclerView() - setupUiLogic() + setupDataBinding() + setupToolbar() + setupMyCommentsRecyclerView() + + observeComments() } - fun initDataBinding() { + private fun setupDataBinding() { binding.viewModel = viewModel - binding.lifecycleOwner = this } - private fun initToolbar() { - binding.tvMycommentsToolbar.setNavigationOnClickListener { finish() } + private fun setupToolbar() { + binding.tbMycommentsToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } - private fun initMyCommentsRecyclerView() { + private fun setupMyCommentsRecyclerView() { binding.rvMycommentsMycomments.apply { - adapter = MyCommentsAdapter(::navigateToChildComments) + adapter = MyCommentsAdapter(::navigateToFeedDetail) itemAnimator = null addItemDecoration(DividerItemDecoration(this@MyCommentsActivity)) } } - private fun navigateToChildComments(eventId: Long, parentCommentId: Long, commentId: Long) { - ChildCommentActivity.startActivity( + private fun navigateToFeedDetail(feedId: Long, commentId: Long) { + FeedDetailActivity.startActivity( context = this, - feedId = eventId, - parentCommentId = parentCommentId, + feedId = feedId, highlightCommentId = commentId, - fromPostDetail = false, ) } - private fun setupUiLogic() { - setupLoginUiLogic() - setupCommentsUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(this) { - handleNotLogin(it) - } - } - - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(this) - finish() - } - } - - private fun setupCommentsUiLogic() { + private fun observeComments() { viewModel.comments.observe(this) { - handleErrors(it) handleComments(it) } } - private fun handleErrors(comments: MyCommentsUiState) { - handleFetchingError(comments) - } - - private fun handleFetchingError(comments: MyCommentsUiState) { - if (comments.isError) { - binding.root.showSnackBar(getString(R.string.comments_comments_fetching_error_message)) - } + private fun handleComments(comments: List) { + (binding.rvMycommentsMycomments.adapter as MyCommentsAdapter).submitList(comments) } - private fun handleComments(comments: MyCommentsUiState) { - (binding.rvMycommentsMycomments.adapter as MyCommentsAdapter).submitList(comments.comments) + override fun onRestart() { + super.onRestart() + viewModel.refresh() } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsViewModel.kt index 5e13fadd0..d5149d8b4 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/MyCommentsViewModel.kt @@ -1,52 +1,48 @@ package com.emmsale.presentation.ui.myCommentList -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Comment import com.emmsale.data.repository.interfaces.CommentRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentsUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class MyCommentsViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val commentRepository: CommentRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin + private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _comments = NotNullMutableLiveData(MyCommentsUiState.FIRST_LOADING) - val comments: NotNullLiveData = _comments + private val _comments = NotNullMutableLiveData(listOf()) + val comments: NotNullLiveData> = _comments init { - refresh() + fetchMyComments() } - override fun refresh() { - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - - when (val result = commentRepository.getCommentsByMemberId(token.uid)) { - is Failure, NetworkError -> _comments.value = _comments.value.changeToErrorState() - is Success -> - _comments.value = _comments.value.setCommentsState(result.data, token.uid) - - is Unexpected -> throw Throwable(result.error) - } - } - } + private fun fetchMyComments(): Job = fetchData( + fetchData = { commentRepository.getCommentsByMemberId(uid) }, + onSuccess = { + _comments.value = it.extractMyComments() + .sortedByDescending { comment -> comment.createdAt } + }, + ) + + override fun refresh(): Job = refreshData( + refresh = { commentRepository.getCommentsByMemberId(uid) }, + onSuccess = { + _comments.value = it.extractMyComments() + .sortedByDescending { comment -> comment.createdAt } + }, + ) + + private fun List.extractMyComments(): List = + flatMap { comment -> + listOf(comment) + comment.childComments + }.filter { comment -> comment.writer.id == uid && !comment.isDeleted } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt index ca47e4a66..a1a0351cd 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentViewHolder.kt @@ -3,31 +3,31 @@ package com.emmsale.presentation.ui.myCommentList.recyclerView import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.emmsale.data.model.Comment import com.emmsale.databinding.ItemMycommentsCommentBinding -import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentUiState class MyCommentViewHolder( private val binding: ItemMycommentsCommentBinding, - onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, + onCommentClick: (feedId: Long, commentId: Long) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { - binding.onClick = onClick + binding.onCommentClick = onCommentClick } - fun bind(comment: MyCommentUiState) { + fun bind(comment: Comment) { binding.comment = comment } companion object { fun create( parent: ViewGroup, - onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, + onCommentClick: (feedId: Long, commentId: Long) -> Unit, ): MyCommentViewHolder { val binding = ItemMycommentsCommentBinding .inflate(LayoutInflater.from(parent.context), parent, false) - return MyCommentViewHolder(binding, onClick) + return MyCommentViewHolder(binding, onCommentClick) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt index 30c2aaa4e..4830cb9ad 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/recyclerView/MyCommentsAdapter.kt @@ -3,14 +3,14 @@ package com.emmsale.presentation.ui.myCommentList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.myCommentList.uiState.MyCommentUiState +import com.emmsale.data.model.Comment class MyCommentsAdapter( - private val onClick: (eventId: Long, parentCommentId: Long, commentId: Long) -> Unit, -) : ListAdapter(diffUtil) { + private val onCommentClick: (feedId: Long, commentId: Long) -> Unit, +) : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyCommentViewHolder { - return MyCommentViewHolder.create(parent, onClick) + return MyCommentViewHolder.create(parent, onCommentClick) } override fun onBindViewHolder(holder: MyCommentViewHolder, position: Int) { @@ -18,15 +18,15 @@ class MyCommentsAdapter( } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { + val diffUtil = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: MyCommentUiState, - newItem: MyCommentUiState, + oldItem: Comment, + newItem: Comment, ): Boolean = oldItem.id == newItem.id override fun areContentsTheSame( - oldItem: MyCommentUiState, - newItem: MyCommentUiState, + oldItem: Comment, + newItem: Comment, ): Boolean = oldItem == newItem } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt deleted file mode 100644 index f3da018af..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentUiState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.emmsale.presentation.ui.myCommentList.uiState - -import com.emmsale.data.model.Comment -import java.time.format.DateTimeFormatter - -data class MyCommentUiState( - val id: Long, - val feedId: Long, - val feedTitle: String, - val authorId: Long, - val parentId: Long?, - val content: String, - val childCount: Int, - val lastModifiedDate: String, - val isUpdated: Boolean, - val isDeleted: Boolean, -) { - companion object { - private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") - - fun from(comment: Comment) = MyCommentUiState( - id = comment.id, - feedId = comment.feed.id, - feedTitle = comment.feed.title, - authorId = comment.writer.id, - parentId = comment.parentCommentId, - content = comment.content, - childCount = comment.childComments.size, - lastModifiedDate = comment.updatedAt.format(dateTimeFormatter), - isUpdated = comment.createdAt != comment.updatedAt, - isDeleted = comment.isDeleted, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentsUiState.kt deleted file mode 100644 index e986dd662..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myCommentList/uiState/MyCommentsUiState.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.emmsale.presentation.ui.myCommentList.uiState - -import com.emmsale.data.model.Comment - -data class MyCommentsUiState( - val isLoading: Boolean, - val isError: Boolean, - val comments: List, -) { - - fun changeToErrorState(): MyCommentsUiState = copy( - isLoading = false, - isError = true, - ) - - fun setCommentsState(newComments: List, loginMemberId: Long): MyCommentsUiState = copy( - isLoading = false, - isError = false, - comments = newComments.flatMap { comment -> - comment.childComments.map(MyCommentUiState.Companion::from) + MyCommentUiState.from( - comment, - ) - }.filter { it.authorId == loginMemberId && !it.isDeleted }, - ) - - companion object { - val FIRST_LOADING = MyCommentsUiState( - isLoading = true, - isError = false, - comments = listOf(), - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileFragment.kt index f9b1ff17d..773be2cfc 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileFragment.kt @@ -1,104 +1,95 @@ package com.emmsale.presentation.ui.myProfile +import android.app.Activity.RESULT_OK import android.os.Bundle import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.viewModels import com.emmsale.R +import com.emmsale.data.model.Activity import com.emmsale.databinding.FragmentMyProfileBinding -import com.emmsale.presentation.base.BaseFragment +import com.emmsale.presentation.base.NetworkFragment +import com.emmsale.presentation.common.extension.dp +import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration import com.emmsale.presentation.common.views.CategoryTagChip import com.emmsale.presentation.ui.editMyProfile.EditMyProfileActivity -import com.emmsale.presentation.ui.login.LoginActivity -import com.emmsale.presentation.ui.myProfile.uiState.MyProfileUiState import com.emmsale.presentation.ui.profile.recyclerView.ActivitiesAdapter -import com.emmsale.presentation.ui.profile.recyclerView.ActivitiesAdapterDecoration import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MyProfileFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_my_profile - private val viewModel: MyProfileViewModel by viewModels() +class MyProfileFragment : NetworkFragment(R.layout.fragment_my_profile) { + + override val viewModel: MyProfileViewModel by viewModels() + + private val editMyProfileActivityLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != RESULT_OK) return@registerForActivityResult + viewModel.refresh() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initDataBinding() - setupUiLogic() - initToolbar() - initActivitiesRecyclerView() - } + setupDataBinding() + setupToolbar() + setupActivitiesRecyclerView() - override fun onStart() { - super.onStart() - viewModel.refresh() + observeProfile() } - private fun initDataBinding() { + private fun setupDataBinding() { binding.viewModel = viewModel } - private fun setupUiLogic() { - setupLoginUiLogic() - setupMyProfileUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(viewLifecycleOwner) { - handleNotLogin(it) + private fun setupToolbar() { + binding.tbMyprofileToolbar.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.myprofile_edit_mode -> { + val intent = EditMyProfileActivity.getIntent(requireContext()) + editMyProfileActivityLauncher.launch(intent) + } + } + true } } - private fun setupMyProfileUiLogic() { - viewModel.myProfile.observe(viewLifecycleOwner) { - handleFields(it) - handleActivities(it) + private fun setupActivitiesRecyclerView() { + val decoration = IntervalItemDecoration(height = 13.dp) + listOf( + binding.rvMyprofileEducations, + binding.rvMyprofileClubs, + ).forEach { + it.apply { + adapter = ActivitiesAdapter() + itemAnimator = null + addItemDecoration(decoration) + } } } - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(requireContext()) - activity?.finish() + private fun observeProfile() { + viewModel.profile.observe(viewLifecycleOwner) { member -> + handleFields(member.fields) + handleEducations(member.educations) + handleClubs(member.clubs) } } - private fun handleFields(myProfile: MyProfileUiState) { + private fun handleFields(fields: List) { binding.cgMyprofileFields.removeAllViews() - myProfile.member.fields.forEach { + fields.forEach { val tagView = CategoryTagChip(requireContext()).apply { text = it.name } binding.cgMyprofileFields.addView(tagView) } } - private fun handleActivities(myProfile: MyProfileUiState) { - (binding.rvMyprofileEducations.adapter as ActivitiesAdapter).submitList( - myProfile.member.educations, - ) - (binding.rvMyprofileClubs.adapter as ActivitiesAdapter).submitList(myProfile.member.clubs) + private fun handleEducations(educations: List) { + (binding.rvMyprofileEducations.adapter as ActivitiesAdapter).submitList(educations) } - private fun initToolbar() { - binding.tbMyprofileToolbar.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.myprofile_edit_mode -> EditMyProfileActivity.startActivity(requireContext()) - } - true - } - } - - private fun initActivitiesRecyclerView() { - val decoration = ActivitiesAdapterDecoration() - listOf( - binding.rvMyprofileEducations, - binding.rvMyprofileClubs, - ).forEach { - it.apply { - adapter = ActivitiesAdapter() - itemAnimator = null - addItemDecoration(decoration) - } - } + private fun handleClubs(clubs: List) { + (binding.rvMyprofileClubs.adapter as ActivitiesAdapter).submitList(clubs) } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileViewModel.kt index ad7015db6..e626efe22 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/MyProfileViewModel.kt @@ -1,49 +1,37 @@ package com.emmsale.presentation.ui.myProfile -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Member import com.emmsale.data.repository.interfaces.MemberRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.myProfile.uiState.MyProfileUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class MyProfileViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val memberRepository: MemberRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin + private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _myProfile = NotNullMutableLiveData(MyProfileUiState()) - val myProfile: NotNullLiveData = _myProfile + private val _profile = NotNullMutableLiveData(Member()) + val profile: NotNullLiveData = _profile - override fun refresh() { - _myProfile.value = _myProfile.value.changeToLoadingState() - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } + init { + fetchProfile() + } - when (val result = memberRepository.getMember(token.uid)) { - is Failure, NetworkError -> - _myProfile.value = _myProfile.value.changeToErrorState() + private fun fetchProfile(): Job = fetchData( + fetchData = { memberRepository.getMember(uid) }, + onSuccess = { _profile.value = it }, + ) - is Success -> _myProfile.value = _myProfile.value.changeMemberState(result.data) - is Unexpected -> throw Throwable(result.error) - } - } - } + override fun refresh(): Job = refreshData( + refresh = { memberRepository.getMember(uid) }, + onSuccess = { _profile.value = it }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/uiState/MyProfileUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/uiState/MyProfileUiState.kt deleted file mode 100644 index fc6ad1c02..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myProfile/uiState/MyProfileUiState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.emmsale.presentation.ui.myProfile.uiState - -import com.emmsale.data.model.Member - -data class MyProfileUiState( - val isLoading: Boolean = true, - val isError: Boolean = false, - val member: Member = Member(), -) { - - fun changeMemberState(member: Member): MyProfileUiState = copy( - isLoading = false, - isError = false, - member = member, - ) - - fun changeToLoadingState(): MyProfileUiState = copy( - isLoading = true, - ) - - fun changeToErrorState(): MyProfileUiState = copy( - isError = true, - ) -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt index f1603a2af..699a43f7e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt @@ -11,7 +11,7 @@ import com.emmsale.R import com.emmsale.databinding.ActivityMyPostBinding import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.ui.myRecruitmentList.recyclerView.MyRecruitmentAdapter -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentPostDetailActivity +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -55,13 +55,11 @@ class MyRecruitmentActivity : AppCompatActivity() { } private fun navigateToDetail(eventId: Long, recruitmentId: Long) { - val intent = RecruitmentPostDetailActivity.getIntent( + RecruitmentDetailActivity.startActivity( this, eventId = eventId, recruitmentId = recruitmentId, - isNavigatedFromMyPost = true, ) - fetchByResultActivityLauncher.launch(intent) } private fun initBackPressButton() { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigActivity.kt index 34c6917bc..01d67a53d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigActivity.kt @@ -5,12 +5,11 @@ import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.emmsale.R import com.emmsale.data.model.EventTag import com.emmsale.databinding.ActivityNotificationConfigBinding -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.checkPostNotificationPermission import com.emmsale.presentation.common.extension.navigateToNotificationSettings import com.emmsale.presentation.common.extension.showPermissionRequestDialog @@ -21,16 +20,15 @@ import com.emmsale.presentation.common.views.CancelablePrimaryTag import com.emmsale.presentation.common.views.ConfirmDialog import com.emmsale.presentation.common.views.cancelablePrimaryChipOf import com.emmsale.presentation.ui.notificationConfig.uiState.NotificationConfigUiEvent -import com.emmsale.presentation.ui.notificationConfig.uiState.NotificationTagsUiState import com.emmsale.presentation.ui.notificationTagConfig.NotificationTagConfigActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NotificationConfigActivity : - AppCompatActivity(), + NetworkActivity(R.layout.activity_notification_config), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("notification_config") { - private val binding by lazy { ActivityNotificationConfigBinding.inflate(layoutInflater) } - private val viewModel: NotificationConfigViewModel by viewModels() + + override val viewModel: NotificationConfigViewModel by viewModels() private val notiTagConfigLauncher = registerForActivityResult(StartActivityForResult()) { viewModel.fetchNotificationTags() @@ -42,60 +40,35 @@ class NotificationConfigActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) + registerScreen(this) - initView() - setupObservers() - } - private fun initView() { - binding.viewModel = viewModel - binding.lifecycleOwner = this - initClickListener() - initNotificationConfigSwitch() - } + setupDateBinding() + setupToolbar() - private fun initClickListener() { - initToolbarMenuClickListener() - initTagAddButtonClickListener() - } - - private fun initToolbarMenuClickListener() { - binding.tbNotificationConfig.setOnMenuItemClickListener { - if (it.itemId == R.id.close) finish() - true - } + observeConfig() + observeNotificationTags() + observeUiEvent() } - private fun initTagAddButtonClickListener() { - binding.btnTagAdd.setOnClickListener { - navigateToNotificationTagConfigActivity() - } - } - - private fun navigateToNotificationTagConfigActivity() { - notiTagConfigLauncher.launch(NotificationTagConfigActivity.getIntent(this)) - } + private fun setupDateBinding() { + binding.viewModel = viewModel + binding.onTagAddButtonClick = + { notiTagConfigLauncher.launch(NotificationTagConfigActivity.getIntent(this)) } - private fun initNotificationConfigSwitch() { - binding.switchAllNotificationReceiveConfig.setOnCheckedChangeListener { _, isChecked -> + binding.onAllNotificationReceiveConfigSwitchClick = { isChecked -> if (!checkPostNotificationPermission() && isChecked) { showPermissionRequestDialog() - return@setOnCheckedChangeListener + } else { + viewModel.setAllNotificationReceiveConfig(isChecked) } - viewModel.setAllNotificationReceiveConfig(isChecked) - } - binding.switchFollowNotificationReceiveConfig.setOnCheckedChangeListener { _, isChecked -> - viewModel.setFollowNotificationReceiveConfig(isChecked) - } - binding.switchCommentNotificationReceiveConfig.setOnCheckedChangeListener { _, isChecked -> - viewModel.setCommentNotificationReceiveConfig(isChecked) - } - binding.switchInterestTagNotificationReceiveConfig.setOnCheckedChangeListener { _, isChecked -> - viewModel.setInterestEventNotificationReceiveConfig(isChecked) - } - binding.switchMessageNotificationReceiveConfig.setOnCheckedChangeListener { _, isChecked -> - viewModel.setMessageNotificationReceiveConfig(isChecked) } + + binding.onInterestTagConfigSwitchClick = + { viewModel.setInterestEventNotificationReceiveConfig(it) } + + binding.onCommentConfigSwitchClick = { viewModel.setCommentNotificationReceiveConfig(it) } + binding.onMessageConfigSwitchClick = { viewModel.setMessageNotificationReceiveConfig(it) } } private fun showPermissionRequestDialog() { @@ -105,13 +78,14 @@ class NotificationConfigActivity : ) } - private fun setupObservers() { - setupConfigObserver() - setupNotificationTagsObserver() - setupNotificationConfigUiEventObserver() + private fun setupToolbar() { + binding.tbNotificationConfig.setOnMenuItemClickListener { + if (it.itemId == R.id.close) finish() + true + } } - private fun setupConfigObserver() { + private fun observeConfig() { viewModel.notificationConfig.observe(this) { config -> val isNotificationPermissionChecked = checkPostNotificationPermission() val isNotificationReceive = config.isNotificationReceive @@ -122,9 +96,6 @@ class NotificationConfigActivity : binding.clNotificationChannelSetting.isVisible = isNotificationPermissionChecked && isNotificationReceive - binding.switchFollowNotificationReceiveConfig.isChecked = - config.isFollowNotificationReceive - binding.switchCommentNotificationReceiveConfig.isChecked = config.isCommentNotificationReceive @@ -136,29 +107,8 @@ class NotificationConfigActivity : } } - private fun setupNotificationTagsObserver() { - viewModel.notificationTags.observe(this) { uiState -> - if (uiState !is NotificationTagsUiState.Success) return@observe - updateNotificationTagViews(uiState.tags) - } - } - - private fun setupNotificationConfigUiEventObserver() { - viewModel.uiEvent.observe(this) { event -> - handleNotificationTagsErrors(event) - } - } - - private fun handleNotificationTagsErrors(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content) { - NotificationConfigUiEvent.INTEREST_TAG_REMOVE_ERROR -> showTagRemovingErrorMessage() - NotificationConfigUiEvent.NONE -> {} - } - } - - private fun showTagRemovingErrorMessage() { - binding.root.showSnackBar(R.string.notificationconfig_tag_removing_error_message) + private fun observeNotificationTags() { + viewModel.notificationTags.observe(this, ::updateNotificationTagViews) } private fun updateNotificationTagViews(conferenceTags: List) { @@ -194,10 +144,20 @@ class NotificationConfigActivity : context = this, title = getString(R.string.notificationconfig_tag_remove_confirm_dialog_title), message = getString(R.string.notificationconfig_tag_remove_confirm_dialog_message), - onPositiveButtonClick = { viewModel.removeInterestTagById(tagId) }, + onPositiveButtonClick = { viewModel.removeNotificationTag(tagId) }, ).show() } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: NotificationConfigUiEvent) { + when (uiEvent) { + NotificationConfigUiEvent.InterestTagRemoveFail -> binding.root.showSnackBar(R.string.notificationconfig_tag_removing_error_message) + } + } + companion object { fun startActivity(context: Context) { context.startActivity(Intent(context, NotificationConfigActivity::class.java)) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigViewModel.kt index f32fda270..6d9111979 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/NotificationConfigViewModel.kt @@ -1,23 +1,20 @@ package com.emmsale.presentation.ui.notificationConfig -import androidx.lifecycle.ViewModel +import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.model.Config +import com.emmsale.data.model.EventTag import com.emmsale.data.repository.interfaces.ConfigRepository import com.emmsale.data.repository.interfaces.EventTagRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.firebase.analytics.logChangeConfig import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.notificationConfig.uiState.NotificationConfigUiEvent -import com.emmsale.presentation.ui.notificationConfig.uiState.NotificationTagsUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import javax.inject.Inject @@ -26,11 +23,13 @@ class NotificationConfigViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val eventTagRepository: EventTagRepository, private val configRepository: ConfigRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { + + private val uid: Long by lazy { tokenRepository.getMyUid()!! } + private val _notificationConfig: NotNullMutableLiveData = NotNullMutableLiveData( Config( isNotificationReceive = false, - isFollowNotificationReceive = false, isCommentNotificationReceive = false, isInterestEventNotificationReceive = false, isAutoLogin = false, @@ -39,95 +38,65 @@ class NotificationConfigViewModel @Inject constructor( ) val notificationConfig: NotNullLiveData = _notificationConfig - private val _notificationTags: NotNullMutableLiveData = - NotNullMutableLiveData(NotificationTagsUiState.Loading) - val notificationTags: NotNullLiveData = _notificationTags + private val _notificationTags = NotNullMutableLiveData(listOf()) + val notificationTags: NotNullLiveData> = _notificationTags - private val _uiEvent: NotNullMutableLiveData> = - NotNullMutableLiveData(UiEvent(NotificationConfigUiEvent.NONE)) - val uiEvent: NotNullLiveData> = _uiEvent + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent init { fetchNotificationTags() fetchNotificationConfig() } - override fun refresh() { - fetchNotificationConfig() - fetchNotificationTags() - } - - fun fetchNotificationTags() { - viewModelScope.launch { - val memberId = tokenRepository.getToken()?.uid ?: return@launch + fun fetchNotificationTags(): Job = fetchData( + fetchData = { eventTagRepository.getInterestEventTags(uid) }, + onSuccess = { _notificationTags.value = it }, + ) - when (val result = eventTagRepository.getInterestEventTags(memberId)) { - is Failure, NetworkError -> _notificationTags.value = NotificationTagsUiState.Error - is Success -> _notificationTags.value = NotificationTagsUiState.Success(result.data) - is Unexpected -> throw Throwable(result.error) - } - } + private fun fetchNotificationConfig(): Job = viewModelScope.launch { + _notificationConfig.value = configRepository.getConfig() } - private fun fetchNotificationConfig() { - viewModelScope.launch { - _notificationConfig.value = configRepository.getConfig() - } + override fun refresh(): Job { + fetchNotificationConfig() + return refreshNotificationTags() } - fun setAllNotificationReceiveConfig(isReceive: Boolean) { - viewModelScope.launch { - configRepository.saveAllNotificationReceiveConfig(isReceive) - fetchNotificationConfig() - logChangeConfig("notification_receive", isReceive) - } - } + private fun refreshNotificationTags(): Job = refreshData( + refresh = { eventTagRepository.getInterestEventTags(uid) }, + onSuccess = { _notificationTags.value = it }, + ) - fun setFollowNotificationReceiveConfig(isReceive: Boolean) { - viewModelScope.launch { - configRepository.saveFollowNotificationReceiveConfig(isReceive) - fetchNotificationConfig() - } + fun setAllNotificationReceiveConfig(isReceive: Boolean): Job = viewModelScope.launch { + configRepository.saveAllNotificationReceiveConfig(isReceive) + fetchNotificationConfig() + logChangeConfig("notification_receive", isReceive) } - fun setCommentNotificationReceiveConfig(isReceive: Boolean) { - viewModelScope.launch { - configRepository.saveCommentNotificationReceiveConfig(isReceive) - fetchNotificationConfig() - } + fun setCommentNotificationReceiveConfig(isReceive: Boolean): Job = viewModelScope.launch { + configRepository.saveCommentNotificationReceiveConfig(isReceive) + fetchNotificationConfig() } - fun setInterestEventNotificationReceiveConfig(isReceive: Boolean) { - viewModelScope.launch { - configRepository.saveInterestEventNotificationReceiveConfig(isReceive) - fetchNotificationConfig() - } + fun setInterestEventNotificationReceiveConfig(isReceive: Boolean): Job = viewModelScope.launch { + configRepository.saveInterestEventNotificationReceiveConfig(isReceive) + fetchNotificationConfig() } - fun setMessageNotificationReceiveConfig(isReceive: Boolean) { - viewModelScope.launch { - configRepository.saveMessageNotificationReceiveConfig(isReceive) - fetchNotificationConfig() - } + fun setMessageNotificationReceiveConfig(isReceive: Boolean): Job = viewModelScope.launch { + configRepository.saveMessageNotificationReceiveConfig(isReceive) + fetchNotificationConfig() } - fun removeInterestTagById(eventTagId: Long) { - viewModelScope.launch { - val notificationTags = _notificationTags.value - if (notificationTags !is NotificationTagsUiState.Success) return@launch - val removedInterestEventTag = - notificationTags.tags.filter { tag -> tag.id != eventTagId } - - when ( - val result = - eventTagRepository.updateInterestEventTags(removedInterestEventTag) - ) { - is Failure, NetworkError -> - _uiEvent.value = UiEvent(NotificationConfigUiEvent.INTEREST_TAG_REMOVE_ERROR) - - is Success -> fetchNotificationTags() - is Unexpected -> throw Throwable(result.error) - } - } - } + fun removeNotificationTag(eventTagId: Long): Job = command( + command = { + val newTags = _notificationTags.value.filter { it.id != eventTagId } + eventTagRepository.updateInterestEventTags(newTags) + }, + onSuccess = { + _notificationTags.value = _notificationTags.value.filter { it.id != eventTagId } + }, + onFailure = { _, _ -> _uiEvent.value = NotificationConfigUiEvent.InterestTagRemoveFail }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/uiState/NotificationConfigUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/uiState/NotificationConfigUiEvent.kt index 7637f0f02..a04881c35 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/uiState/NotificationConfigUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationConfig/uiState/NotificationConfigUiEvent.kt @@ -1,5 +1,5 @@ package com.emmsale.presentation.ui.notificationConfig.uiState -enum class NotificationConfigUiEvent { - NONE, INTEREST_TAG_REMOVE_ERROR +sealed interface NotificationConfigUiEvent { + object InterestTagRemoveFail : NotificationConfigUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsActivity.kt new file mode 100644 index 000000000..f4bcd936e --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsActivity.kt @@ -0,0 +1,135 @@ +package com.emmsale.presentation.ui.notificationList + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.recyclerview.widget.ConcatAdapter +import com.emmsale.R +import com.emmsale.data.model.notification.ChildCommentNotification +import com.emmsale.data.model.notification.InterestEventNotification +import com.emmsale.data.model.notification.Notification +import com.emmsale.databinding.ActivityNotificationsBinding +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.views.WarningDialog +import com.emmsale.presentation.ui.eventDetail.EventDetailActivity +import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity +import com.emmsale.presentation.ui.notificationList.recyclerView.adapter.NotificationsAdapter +import com.emmsale.presentation.ui.notificationList.recyclerView.adapter.PastNotificationHeaderAdapter +import com.emmsale.presentation.ui.notificationList.recyclerView.adapter.RecentNotificationHeaderAdapter +import com.emmsale.presentation.ui.notificationList.uiState.NotificationsUiEvent +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NotificationsActivity : + NetworkActivity(R.layout.activity_notifications) { + + override val viewModel: NotificationsViewModel by viewModels() + + private val recentNotificationHeaderAdapter = RecentNotificationHeaderAdapter() + private val recentNotificationAdapter = NotificationsAdapter( + onNotificationClick = { notification -> + viewModel.readNotification(notification.id) + navigateToDetailScreen(notification) + }, + onDeleteClick = { viewModel.deleteNotification(it) }, + ) + + private val pastNotificationHeaderAdapter = PastNotificationHeaderAdapter( + onDeleteAllNotificationClick = ::showNotificationDeleteConfirmDialog, + ) + private val pastNotificationAdapter = NotificationsAdapter( + onNotificationClick = ::navigateToDetailScreen, + onDeleteClick = { viewModel.deleteNotification(it) }, + ) + + private fun navigateToDetailScreen(notification: Notification) { + when (notification) { + is ChildCommentNotification -> navigateToCommentScreen( + feedId = notification.comment.feed.id, + commentId = notification.comment.id, + ) + + is InterestEventNotification -> navigateToEventScreen(notification.event.id) + } + } + + private fun navigateToEventScreen(eventId: Long) { + EventDetailActivity.startActivity(this, eventId) + } + + private fun navigateToCommentScreen(feedId: Long, commentId: Long) { + FeedDetailActivity.startActivity( + context = this, + feedId = feedId, + highlightCommentId = commentId, + ) + } + + private fun showNotificationDeleteConfirmDialog() { + WarningDialog( + context = this, + title = getString(R.string.notifications_delete_notification_confirm_title), + message = getString(R.string.notifications_delete_notification_confirm_message), + positiveButtonLabel = getString(R.string.all_okay), + negativeButtonLabel = getString(R.string.all_cancel), + onPositiveButtonClick = { viewModel.deleteAllPastNotifications() }, + ).show() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setupDateBinding() + setupToolbar() + setupNotificationsRecyclerView() + + observeNotifications() + observeUiEvent() + } + + private fun setupDateBinding() { + binding.viewModel = viewModel + } + + private fun setupToolbar() { + binding.tbNotifications.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + } + + private fun setupNotificationsRecyclerView() { + val concatAdapterConfig = ConcatAdapter.Config.Builder().setIsolateViewTypes(false).build() + + binding.rvNotifications.adapter = ConcatAdapter( + concatAdapterConfig, + recentNotificationHeaderAdapter, + recentNotificationAdapter, + pastNotificationHeaderAdapter, + pastNotificationAdapter, + ) + } + + private fun observeNotifications() { + viewModel.notifications.observe(this) { + recentNotificationAdapter.submitList(it.recentNotifications) + pastNotificationAdapter.submitList(it.pastNotifications) + } + } + + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: NotificationsUiEvent) { + when (uiEvent) { + NotificationsUiEvent.DeleteFail -> binding.root.showSnackBar(R.string.notifications_delete_notification_failed_message) + } + } + + companion object { + fun startActivity(context: Context) { + Intent(context, NotificationsActivity::class.java).run { context.startActivity(this) } + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsViewModel.kt new file mode 100644 index 000000000..2936d85fe --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/NotificationsViewModel.kt @@ -0,0 +1,68 @@ +package com.emmsale.presentation.ui.notificationList + +import androidx.lifecycle.LiveData +import com.emmsale.data.repository.interfaces.NotificationRepository +import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.notificationList.uiState.NotificationsUiEvent +import com.emmsale.presentation.ui.notificationList.uiState.NotificationsUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject + +@HiltViewModel +class NotificationsViewModel @Inject constructor( + private val tokenRepository: TokenRepository, + private val notificationRepository: NotificationRepository, +) : RefreshableViewModel() { + + private val uid: Long by lazy { tokenRepository.getMyUid()!! } + + private val _notifications = NotNullMutableLiveData(NotificationsUiState()) + val notifications: NotNullLiveData = _notifications + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent + + init { + fetchNotifications() + } + + private fun fetchNotifications(): Job = fetchData( + fetchData = { notificationRepository.getNotifications(uid) }, + onSuccess = { _notifications.value = NotificationsUiState(it) }, + ) + + override fun refresh(): Job = refreshData( + refresh = { notificationRepository.getNotifications(uid) }, + onSuccess = { _notifications.value = NotificationsUiState(it) }, + ) + + fun deleteAllPastNotifications(): Job = command( + command = { + val pastNotificationIds = + _notifications.value.pastNotifications.map { it.id } + notificationRepository.deleteNotifications(pastNotificationIds) + }, + onSuccess = { _notifications.value = _notifications.value.deleteAllPastNotifications() }, + onFailure = { _, _ -> _uiEvent.value = NotificationsUiEvent.DeleteFail }, + ) + + fun readNotification(notificationId: Long): Job = command( + command = { notificationRepository.readNotification(notificationId) }, + onSuccess = { + _notifications.value = _notifications.value.readNotification(notificationId) + }, + ) + + fun deleteNotification(notificationId: Long): Job = command( + command = { notificationRepository.deleteNotifications(listOf(notificationId)) }, + onSuccess = { + _notifications.value = _notifications.value.deleteNotification(notificationId) + }, + onFailure = { _, _ -> _uiEvent.value = NotificationsUiEvent.DeleteFail }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/NotificationsAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/NotificationsAdapter.kt new file mode 100644 index 000000000..ff7386d70 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/NotificationsAdapter.kt @@ -0,0 +1,46 @@ +package com.emmsale.presentation.ui.notificationList.recyclerView.adapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.emmsale.data.model.notification.ChildCommentNotification +import com.emmsale.data.model.notification.InterestEventNotification +import com.emmsale.data.model.notification.Notification +import com.emmsale.presentation.ui.notificationList.recyclerView.diffutil.NotificationDiffUtil +import com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder.ChildCommentNotificationViewHolder +import com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder.InterestEventNotificationViewHolder +import com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder.NotificationViewHolder +import com.emmsale.presentation.ui.notificationList.recyclerView.viewtype.NotificationBodyViewType + +class NotificationsAdapter( + private val onNotificationClick: (notification: Notification) -> Unit, + private val onDeleteClick: (notificationId: Long) -> Unit, +) : ListAdapter( + NotificationDiffUtil, +) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): NotificationViewHolder = when (NotificationBodyViewType.of(viewType)) { + NotificationBodyViewType.CHILD_COMMENT -> ChildCommentNotificationViewHolder( + parent = parent, + onNotificationClick = onNotificationClick, + onDeleteNotificationClick = onDeleteClick, + ) + + NotificationBodyViewType.INTEREST_EVENT -> InterestEventNotificationViewHolder( + parent = parent, + onNotificationClick = onNotificationClick, + onDeleteClick = onDeleteClick, + ) + } + + override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun getItemViewType(position: Int): Int = when { + getItem(position) is ChildCommentNotification -> NotificationBodyViewType.CHILD_COMMENT.viewType + getItem(position) is InterestEventNotification -> NotificationBodyViewType.INTEREST_EVENT.viewType + else -> throw IllegalArgumentException("올바르지 않은 ViewType 입니다.") + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt similarity index 81% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt index de5214f4d..7fd129e1b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/PastNotificationHeaderAdapter.kt @@ -1,8 +1,8 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter +package com.emmsale.presentation.ui.notificationList.recyclerView.adapter import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder.PastNotificationsHeaderViewHolder +import com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder.PastNotificationsHeaderViewHolder class PastNotificationHeaderAdapter( private val onDeleteAllNotificationClick: () -> Unit, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt similarity index 78% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt index 758539821..70e78535e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/adapter/RecentNotificationHeaderAdapter.kt @@ -1,8 +1,8 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter +package com.emmsale.presentation.ui.notificationList.recyclerView.adapter import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder.RecentNotificationsHeaderViewHolder +import com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder.RecentNotificationsHeaderViewHolder class RecentNotificationHeaderAdapter : RecyclerView.Adapter() { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/diffutil/NotificationDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/diffutil/NotificationDiffUtil.kt new file mode 100644 index 000000000..ac86c61a3 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/diffutil/NotificationDiffUtil.kt @@ -0,0 +1,16 @@ +package com.emmsale.presentation.ui.notificationList.recyclerView.diffutil + +import androidx.recyclerview.widget.DiffUtil +import com.emmsale.data.model.notification.Notification + +object NotificationDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Notification, + newItem: Notification, + ): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: Notification, + newItem: Notification, + ): Boolean = oldItem == newItem +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt similarity index 57% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt index cc1698eef..484a91a7d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/ChildCommentNotificationViewHolder.kt @@ -1,17 +1,17 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder +package com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup import com.emmsale.R +import com.emmsale.data.model.notification.ChildCommentNotification +import com.emmsale.data.model.notification.Notification import com.emmsale.databinding.ItemCommentNotificationBodyBinding -import com.emmsale.presentation.ui.primaryNotificationList.uiState.ChildCommentNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState class ChildCommentNotificationViewHolder( parent: ViewGroup, - onNotificationClick: (notification: PrimaryNotificationUiState) -> Unit = {}, + onNotificationClick: (notification: Notification) -> Unit = {}, onDeleteNotificationClick: (notificationId: Long) -> Unit = {}, -) : PrimaryNotificationViewHolder( +) : NotificationViewHolder( LayoutInflater.from(parent.context).inflate( R.layout.item_comment_notification_body, parent, @@ -25,8 +25,8 @@ class ChildCommentNotificationViewHolder( binding.onDeleteClick = onDeleteNotificationClick } - override fun bind(item: PrimaryNotificationUiState) { - if (item !is ChildCommentNotificationUiState) return + override fun bind(item: Notification) { + if (item !is ChildCommentNotification) return binding.commentNotification = item } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt new file mode 100644 index 000000000..0da84dc9b --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt @@ -0,0 +1,33 @@ +package com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.emmsale.R +import com.emmsale.data.model.notification.InterestEventNotification +import com.emmsale.data.model.notification.Notification +import com.emmsale.databinding.ItemInterestEventNotificationBinding + +class InterestEventNotificationViewHolder( + parent: ViewGroup, + onNotificationClick: (notification: Notification) -> Unit = {}, + onDeleteClick: (notificationId: Long) -> Unit = {}, +) : NotificationViewHolder( + LayoutInflater.from(parent.context).inflate( + R.layout.item_interest_event_notification, + parent, + false, + ), +) { + private val binding = ItemInterestEventNotificationBinding.bind(itemView) + + init { + binding.onNotificationClick = onNotificationClick + binding.onDeleteClick = onDeleteClick + } + + override fun bind(item: Notification) { + if (item !is InterestEventNotification) return + + binding.interestEventNotification = item + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/NotificationViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/NotificationViewHolder.kt new file mode 100644 index 000000000..067a78261 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/NotificationViewHolder.kt @@ -0,0 +1,9 @@ +package com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.emmsale.data.model.notification.Notification + +abstract class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun bind(item: Notification) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt similarity index 88% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt index 7a1a5a76b..307bf84c5 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/PastNotificationsHeaderViewHolder.kt @@ -1,4 +1,4 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder +package com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt similarity index 81% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt index 938e2bf34..2aeea9d29 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewHolder/RecentNotificationsHeaderViewHolder.kt @@ -1,4 +1,4 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder +package com.emmsale.presentation.ui.notificationList.recyclerView.viewHolder import android.view.LayoutInflater import android.view.ViewGroup diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewtype/PrimaryNotificationBodyViewType.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewtype/NotificationBodyViewType.kt similarity index 62% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewtype/PrimaryNotificationBodyViewType.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewtype/NotificationBodyViewType.kt index 561af8d54..b1d74c5fc 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewtype/PrimaryNotificationBodyViewType.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/recyclerView/viewtype/NotificationBodyViewType.kt @@ -1,6 +1,6 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewtype +package com.emmsale.presentation.ui.notificationList.recyclerView.viewtype -enum class PrimaryNotificationBodyViewType(val viewType: Int) { +enum class NotificationBodyViewType(val viewType: Int) { INTEREST_EVENT(0), CHILD_COMMENT(1), ; @@ -8,7 +8,7 @@ enum class PrimaryNotificationBodyViewType(val viewType: Int) { companion object { private const val INVALID_VIEW_TYPE_ERROR_MESSAGE = "올바르지 않은 ViewType 입니다." - fun of(viewType: Int): PrimaryNotificationBodyViewType = when (viewType) { + fun of(viewType: Int): NotificationBodyViewType = when (viewType) { INTEREST_EVENT.viewType -> INTEREST_EVENT CHILD_COMMENT.viewType -> CHILD_COMMENT else -> throw IllegalArgumentException(INVALID_VIEW_TYPE_ERROR_MESSAGE) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiEvent.kt new file mode 100644 index 000000000..fb77b59f1 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiEvent.kt @@ -0,0 +1,5 @@ +package com.emmsale.presentation.ui.notificationList.uiState + +sealed interface NotificationsUiEvent { + object DeleteFail : NotificationsUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiState.kt new file mode 100644 index 000000000..ecadb127e --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationList/uiState/NotificationsUiState.kt @@ -0,0 +1,38 @@ +package com.emmsale.presentation.ui.notificationList.uiState + +import com.emmsale.data.model.notification.Notification + +data class NotificationsUiState( + val recentNotifications: List = emptyList(), + val pastNotifications: List = emptyList(), +) { + constructor(notifications: List) : this( + recentNotifications = notifications.filter { !it.isRead } + .sortedByDescending { it.createdAt }, + pastNotifications = notifications.filter { it.isRead } + .sortedByDescending { it.createdAt }, + ) + + fun deleteAllPastNotifications() = NotificationsUiState( + recentNotifications = recentNotifications, + pastNotifications = emptyList(), + ) + + fun readNotification(notificationId: Long): NotificationsUiState { + val notification = recentNotifications.find { it.id == notificationId } ?: return this + + val recentNotifications = recentNotifications.filter { it.id != notificationId } + val pastNotifications = (pastNotifications + notification.read()) + .sortedByDescending { it.createdAt } + + return NotificationsUiState( + recentNotifications = recentNotifications, + pastNotifications = pastNotifications, + ) + } + + fun deleteNotification(notificationId: Long) = NotificationsUiState( + recentNotifications = recentNotifications.filter { it.id != notificationId }, + pastNotifications = pastNotifications.filter { it.id != notificationId }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxActivity.kt deleted file mode 100644 index 8bb19f6a7..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxActivity.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.emmsale.presentation.ui.notificationPageList - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.emmsale.databinding.ActivityNotificationBoxBinding -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class NotificationBoxActivity : AppCompatActivity() { - private val binding by lazy { ActivityNotificationBoxBinding.inflate(layoutInflater) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - initView() - } - - private fun initView() { - initNotificationBoxTabLayout() - initBackPressNavigationClickListener() - } - - private fun initNotificationBoxTabLayout() { - initNotificationBoxTabMediator() - } - - private fun initNotificationBoxTabMediator() { - binding.vpNotificationBox.adapter = NotificationBoxFragmentStateAdapter(this) - } - - private fun initBackPressNavigationClickListener() { - binding.tbNotiBox.setNavigationOnClickListener { finish() } - } - - companion object { - fun getIntent(context: Context): Intent = - Intent(context, NotificationBoxActivity::class.java) - - fun startActivity(context: Context) { - context.startActivity(getIntent(context)) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxFragmentStateAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxFragmentStateAdapter.kt deleted file mode 100644 index a4377aa5e..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationPageList/NotificationBoxFragmentStateAdapter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.emmsale.presentation.ui.notificationPageList - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.emmsale.presentation.ui.primaryNotificationList.PrimaryNotificationFragment - -class NotificationBoxFragmentStateAdapter( - activity: FragmentActivity, -) : FragmentStateAdapter(activity) { - override fun getItemCount(): Int = NOTIFICATION_BOX_TAB_COUNT - - override fun createFragment(position: Int): Fragment = when (position) { - PRIMARY_NOTIFICATION_TAB_POSITION -> PrimaryNotificationFragment() - else -> throw IllegalArgumentException(INVALID_FRAGMENT_POSITION_MESSAGE.format(position)) - } - - companion object { - private const val NOTIFICATION_BOX_TAB_COUNT = 1 - private const val PRIMARY_NOTIFICATION_TAB_POSITION = 0 - - private const val INVALID_FRAGMENT_POSITION_MESSAGE = - "%d는 올바르지 않은 Fragment Position입니다." - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigActivity.kt index 8e62bea3e..ff2b53b01 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigActivity.kt @@ -3,10 +3,11 @@ package com.emmsale.presentation.ui.notificationTagConfig import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityNotificationTagConfigBinding +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl @@ -15,37 +16,47 @@ import com.emmsale.presentation.common.views.ConfirmDialog import com.emmsale.presentation.common.views.activityChipOf import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagConfigUiEvent import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagConfigUiState +import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagsConfigUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NotificationTagConfigActivity : - AppCompatActivity(), + NetworkActivity(R.layout.activity_notification_tag_config), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("notification_tag_config") { - private val viewModel: NotificationTagConfigViewModel by viewModels() - private val binding by lazy { ActivityNotificationTagConfigBinding.inflate(layoutInflater) } + + override val viewModel: NotificationTagConfigViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - registerScreen(this) setContentView(binding.root) - initView() + + registerScreen(this) + + setupDateBinding() + setupBackPressedDispatcher() + setupToolbar() + + observeNotificationTags() + observeUiEvent() } - private fun initView() { + private fun setupDateBinding() { binding.viewModel = viewModel - binding.lifecycleOwner = this - initClickListener() - initObservers() } - private fun initClickListener() { - initToolbarNavigationClickListener() + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModel.isChanged.value == true) showFinishConfirmDialog() else finish() + } + }, + ) } - private fun initToolbarNavigationClickListener() { - binding.tbNotificationTagConfig.setNavigationOnClickListener { - showFinishConfirmDialog() - } + private fun setupToolbar() { + binding.tbNotificationTagConfig.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } private fun showFinishConfirmDialog() { @@ -57,64 +68,45 @@ class NotificationTagConfigActivity : ).show() } - private fun initObservers() { - setupNotificationTagsObserver() - setupUiEvent() + private fun observeNotificationTags() { + viewModel.notificationTags.observe(this, ::updateNotificationTagViews) } - private fun setupNotificationTagsObserver() { - viewModel.notificationTags.observe(this) { uiState -> - updateNotificationTagViews(uiState.conferenceTags) - } - } - - private fun updateNotificationTagViews(conferenceTags: List) { - clearNotificationTagViews() - addConferenceTags(conferenceTags) - } - - private fun clearNotificationTagViews() { + private fun updateNotificationTagViews(uiState: NotificationTagsConfigUiState) { binding.cgNotificationTag.removeAllViews() + addEventTags(uiState.eventTags) } - private fun addConferenceTags(conferenceTags: List) { - conferenceTags.forEach(::addConferenceTag) + private fun addEventTags(eventTags: List) { + eventTags.forEach(::addEventTag) } - private fun addConferenceTag(conferenceTag: NotificationTagConfigUiState) { - binding.cgNotificationTag.addView(createEventTag(conferenceTag)) + private fun addEventTag(eventTags: NotificationTagConfigUiState) { + binding.cgNotificationTag.addView(createEventTag(eventTags)) } private fun createEventTag(eventTag: NotificationTagConfigUiState): ActivityTag = activityChipOf { - text = eventTag.tagName + text = eventTag.eventTag.name isChecked = eventTag.isChecked setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - viewModel.addInterestTag(eventTag.id) + viewModel.checkTag(eventTag.eventTag.id) } else { - viewModel.removeInterestTag(eventTag.id) + viewModel.uncheckTag(eventTag.eventTag.id) } } } - private fun setupUiEvent() { - viewModel.event.observe(this) { - handleEvent(it) - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun handleEvent(event: NotificationTagConfigUiEvent?) { - if (event == null) return - when (event) { - NotificationTagConfigUiEvent.UPDATE_SUCCESS -> finish() - NotificationTagConfigUiEvent.UPDATE_FAIL -> showInterestTagsUpdateErrorMessage() + private fun handleUiEvent(uiEvent: NotificationTagConfigUiEvent) { + when (uiEvent) { + NotificationTagConfigUiEvent.UpdateComplete -> finish() + NotificationTagConfigUiEvent.UpdateFail -> binding.root.showSnackBar(R.string.notificationtagconfig_interest_tags_update_error_message) } - viewModel.resetEvent() - } - - private fun showInterestTagsUpdateErrorMessage() { - binding.root.showSnackBar(R.string.notificationtagconfig_interest_tags_update_error_message) } companion object { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigViewModel.kt index 803976e33..223ee95dd 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/NotificationTagConfigViewModel.kt @@ -1,8 +1,7 @@ package com.emmsale.presentation.ui.notificationTagConfig import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import com.emmsale.data.common.retrofit.callAdapter.Failure import com.emmsale.data.common.retrofit.callAdapter.NetworkError @@ -11,15 +10,16 @@ import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.model.EventTag import com.emmsale.data.repository.interfaces.EventTagRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.firebase.analytics.logInterestTags +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.CommonUiEvent +import com.emmsale.presentation.common.ScreenUiState import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagConfigUiEvent -import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagConfigUiState import com.emmsale.presentation.ui.notificationTagConfig.uiState.NotificationTagsConfigUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch @@ -29,84 +29,105 @@ import javax.inject.Inject class NotificationTagConfigViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val eventTagRepository: EventTagRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { + + private val uid: Long by lazy { tokenRepository.getMyUid()!! } + + private var originNotificationTags: List = emptyList() + private val _notificationTags = NotNullMutableLiveData(NotificationTagsConfigUiState()) val notificationTags: NotNullLiveData = _notificationTags - private val _event = MutableLiveData(null) - val event: LiveData = _event + val isChanged: LiveData = _notificationTags.map { uiState -> + originNotificationTags != uiState.eventTags + .filter { it.isChecked } + .map { it.eventTag } + } + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent init { - refresh() + fetchAll() } - override fun refresh() { - _notificationTags.value = notificationTags.value.copy(isLoading = true) + private fun fetchAll(): Job = viewModelScope.launch { + changeToLoadingState() + val (eventTagsResult, interestTagsResult) = listOf( + async { eventTagRepository.getEventTags() }, + async { eventTagRepository.getInterestEventTags(uid) }, + ).awaitAll() - viewModelScope.launch { - val memberId = tokenRepository.getToken()?.uid ?: return@launch + when { + eventTagsResult is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(eventTagsResult.error.toString()) - val (eventTags, interestEventTags) = awaitAll( - getEventTagsAsync(), - getInterestEventTagsAsync(memberId), - ) + interestTagsResult is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(interestTagsResult.error.toString()) - if (eventTags == null || interestEventTags == null) { - _notificationTags.value = - _notificationTags.value.copy(isLoading = false, isError = true) + eventTagsResult is Failure || interestTagsResult is Failure -> dispatchFetchFailEvent() + eventTagsResult is NetworkError || interestTagsResult is NetworkError -> { + changeToNetworkErrorState() return@launch } - _notificationTags.value = NotificationTagsConfigUiState.from( - eventTags = eventTags, - interestEventTags = interestEventTags, - ) - } - } - private suspend fun getEventTagsAsync(): Deferred?> = viewModelScope.async { - when (val result = eventTagRepository.getEventTags()) { - is Success -> result.data - is Failure, NetworkError -> null - is Unexpected -> throw Throwable(result.error) + eventTagsResult is Success && interestTagsResult is Success -> { + originNotificationTags = interestTagsResult.data + _notificationTags.value = NotificationTagsConfigUiState( + eventTags = eventTagsResult.data, + interestEventTags = interestTagsResult.data, + ) + } } + _screenUiState.value = ScreenUiState.NONE } - private suspend fun getInterestEventTagsAsync(memberId: Long): Deferred?> = - viewModelScope.async { - when (val result = eventTagRepository.getInterestEventTags(memberId)) { - is Failure, NetworkError -> null - is Success -> result.data - is Unexpected -> throw Throwable(result.error) - } - } + override fun refresh(): Job = viewModelScope.launch { + val (eventTagsResult, interestTagsResult) = listOf( + async { eventTagRepository.getEventTags() }, + async { eventTagRepository.getInterestEventTags(uid) }, + ).awaitAll() - fun saveInterestEventTagIds() { - viewModelScope.launch { - val interestEventTags = notificationTags.value.conferenceTags - .filter(NotificationTagConfigUiState::isChecked) - .map { EventTag(id = it.id, name = it.tagName) } + when { + eventTagsResult is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(eventTagsResult.error.toString()) - when (val result = eventTagRepository.updateInterestEventTags(interestEventTags)) { - is Failure, NetworkError -> _event.value = NotificationTagConfigUiEvent.UPDATE_FAIL - is Success -> { - _event.value = NotificationTagConfigUiEvent.UPDATE_SUCCESS - logInterestTags(interestEventTags.map(EventTag::name)) - } + interestTagsResult is Unexpected -> + _commonUiEvent.value = CommonUiEvent.Unexpected(interestTagsResult.error.toString()) - is Unexpected -> throw Throwable(result.error) + eventTagsResult is Failure || interestTagsResult is Failure -> dispatchFetchFailEvent() + eventTagsResult is NetworkError || interestTagsResult is NetworkError -> { + dispatchNetworkErrorEvent() + return@launch } - } - } - fun addInterestTag(tagId: Long) { - _notificationTags.value = notificationTags.value.addInterestTagById(tagId) + eventTagsResult is Success && interestTagsResult is Success -> { + originNotificationTags = interestTagsResult.data + _notificationTags.value = NotificationTagsConfigUiState( + eventTags = eventTagsResult.data, + interestEventTags = interestTagsResult.data, + ) + } + } + _screenUiState.value = ScreenUiState.NONE } - fun removeInterestTag(tagId: Long) { - _notificationTags.value = notificationTags.value.removeInterestTagById(tagId) + fun saveInterestEventTag(): Job = command( + command = { + val interestTags = _notificationTags.value.eventTags + .filter { it.isChecked } + .map { it.eventTag } + eventTagRepository.updateInterestEventTags(interestTags) + }, + onSuccess = { _uiEvent.value = NotificationTagConfigUiEvent.UpdateComplete }, + onFailure = { _, _ -> _uiEvent.value = NotificationTagConfigUiEvent.UpdateFail }, + ) + + fun checkTag(tagId: Long) { + _notificationTags.value = notificationTags.value.checkTag(tagId) } - fun resetEvent() { - _event.value = null + fun uncheckTag(tagId: Long) { + _notificationTags.value = notificationTags.value.uncheckTag(tagId) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiEvent.kt index 61516d45c..86902af7c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiEvent.kt @@ -1,5 +1,6 @@ package com.emmsale.presentation.ui.notificationTagConfig.uiState -enum class NotificationTagConfigUiEvent { - UPDATE_SUCCESS, UPDATE_FAIL +sealed interface NotificationTagConfigUiEvent { + object UpdateComplete : NotificationTagConfigUiEvent + object UpdateFail : NotificationTagConfigUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiState.kt index bad1d3a2b..a71189a64 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagConfigUiState.kt @@ -3,19 +3,6 @@ package com.emmsale.presentation.ui.notificationTagConfig.uiState import com.emmsale.data.model.EventTag data class NotificationTagConfigUiState( - val id: Long, - val tagName: String, + val eventTag: EventTag, val isChecked: Boolean, -) { - fun setChecked(isChecked: Boolean): NotificationTagConfigUiState = - copy(isChecked = isChecked) - - companion object { - fun from(eventTag: EventTag, isSelected: Boolean): NotificationTagConfigUiState = - NotificationTagConfigUiState( - id = eventTag.id, - tagName = eventTag.name, - isChecked = isSelected, - ) - } -} +) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagsConfigUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagsConfigUiState.kt index 4c44c4e41..db6961775 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagsConfigUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/notificationTagConfig/uiState/NotificationTagsConfigUiState.kt @@ -3,40 +3,22 @@ package com.emmsale.presentation.ui.notificationTagConfig.uiState import com.emmsale.data.model.EventTag data class NotificationTagsConfigUiState( - val conferenceTags: List = emptyList(), - val isLoading: Boolean = false, - val isError: Boolean = false, + val eventTags: List = emptyList(), ) { - fun addInterestTagById(id: Long): NotificationTagsConfigUiState = copy( - conferenceTags = conferenceTags.map { tag -> - if (tag.id == id) tag.setChecked(true) else tag - }, - isError = false, + + constructor(eventTags: List, interestEventTags: List) : this( + eventTags.map { NotificationTagConfigUiState(it, it in interestEventTags) }, ) - fun removeInterestTagById(id: Long): NotificationTagsConfigUiState = copy( - conferenceTags = conferenceTags.map { tag -> - if (tag.id == id) tag.setChecked(false) else tag + fun checkTag(tagId: Long) = NotificationTagsConfigUiState( + eventTags = eventTags.map { + if (it.eventTag.id == tagId) it.copy(isChecked = true) else it }, - isError = false, ) - companion object { - fun from( - eventTags: List, - interestEventTags: List, - ): NotificationTagsConfigUiState { - val interestTagIds = interestEventTags.map(EventTag::id) - - return NotificationTagsConfigUiState( - conferenceTags = eventTags.map { eventTag -> - NotificationTagConfigUiState.from( - eventTag = eventTag, - isSelected = eventTag.id in interestTagIds, - ) - }, - isError = false, - ) - } - } + fun uncheckTag(tagId: Long) = NotificationTagsConfigUiState( + eventTags = eventTags.map { + if (it.eventTag.id == tagId) it.copy(isChecked = false) else it + }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt index 0a1a3bc1e..99f6a2510 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingActivity.kt @@ -3,21 +3,21 @@ package com.emmsale.presentation.ui.onboarding import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.View import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityOnboardingBinding +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.showSnackBar import com.emmsale.presentation.ui.main.MainActivity -import com.emmsale.presentation.ui.onboarding.uiState.MemberSavingUiState +import com.emmsale.presentation.ui.onboarding.uiState.OnboardingUiEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class OnboardingActivity : AppCompatActivity() { - private val binding by lazy { ActivityOnboardingBinding.inflate(layoutInflater) } - private val viewModel: OnboardingViewModel by viewModels() +class OnboardingActivity : + NetworkActivity(R.layout.activity_onboarding) { + + override val viewModel: OnboardingViewModel by viewModels() private val fragmentStateAdapter: OnboardingFragmentStateAdapter by lazy { OnboardingFragmentStateAdapter(this) @@ -26,46 +26,51 @@ class OnboardingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - initFragmentStateAdapter() - initBackPressedDispatcher() - setupActivitiesUiState() - } - private fun setupActivitiesUiState() { - viewModel.activities.observe(this) { activities -> - when (activities.memberSavingUiState) { - is MemberSavingUiState.None -> Unit - is MemberSavingUiState.Success -> navigateToMain() - is MemberSavingUiState.Failed -> showMemberUpdateFailed() - } - } + setupFragmentStateAdapter() + setupBackPressedDispatcher() + + observeUiEvent() } - private fun initFragmentStateAdapter() { + private fun setupFragmentStateAdapter() { binding.vpOnboarding.adapter = fragmentStateAdapter } - private fun initBackPressedDispatcher() { - onBackPressedDispatcher.addCallback(this, OnboardingOnBackPressedCallback()) + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + navigateToPrevPage() + } + }, + ) + } + + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: OnboardingUiEvent) { + when (uiEvent) { + OnboardingUiEvent.FieldLimitExceedChecked -> binding.root.showSnackBar(R.string.onboardingfield_selection_limit_exceed) + OnboardingUiEvent.JoinComplete -> navigateToMain() + OnboardingUiEvent.JoinFail -> binding.root.showSnackBar(getString(R.string.onboarding_join_failed_message)) + } } private fun navigateToMain() { - binding.progressbarLoading.visibility = View.GONE MainActivity.startActivity(this) finish() } - private fun showMemberUpdateFailed() { - binding.progressbarLoading.visibility = View.GONE - binding.root.showSnackBar(getString(R.string.onboarding_member_update_failed_message)) - } - fun navigateToNextPage() { val currentPage = binding.vpOnboarding.currentItem val lastPage = fragmentStateAdapter.itemCount - 1 when { currentPage < lastPage -> binding.vpOnboarding.currentItem += 1 - currentPage == lastPage -> viewModel.updateMember() + currentPage == lastPage -> viewModel.join() } } @@ -82,10 +87,4 @@ class OnboardingActivity : AppCompatActivity() { context.startActivity(intent) } } - - inner class OnboardingOnBackPressedCallback : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - navigateToPrevPage() - } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt index d0f390c47..f9ae9ea5e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingClubFragment.kt @@ -11,26 +11,36 @@ import com.emmsale.presentation.ui.onboarding.uiState.ActivityUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class OnboardingClubFragment : BaseFragment(), View.OnClickListener { - override val layoutResId: Int = R.layout.fragment_onboarding_club +class OnboardingClubFragment : + BaseFragment(R.layout.fragment_onboarding_club) { + val viewModel: OnboardingViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupDataBinding() + setupToolbar() + + observeClubs() + } + + private fun setupDataBinding() { binding.viewModel = viewModel - initClickListener() - setupClubs() + binding.onNextButtonClick = + { (requireActivity() as OnboardingActivity).navigateToNextPage() } } - private fun initClickListener() { - binding.btnNext.setOnClickListener(this) - binding.btnBack.setOnClickListener(this) + private fun setupToolbar() { + binding.tbClubFragment.setNavigationOnClickListener { + (requireActivity() as OnboardingActivity).onBackPressedDispatcher.onBackPressed() + } } - private fun setupClubs() { - viewModel.activities.observe(viewLifecycleOwner) { activities -> + private fun observeClubs() { + viewModel.clubs.observe(viewLifecycleOwner) { clubs -> binding.chipgroupClubTags.removeAllViews() - activities.clubs.forEach(::addClubChip) + clubs.forEach(::addClubChip) } } @@ -39,17 +49,10 @@ class OnboardingClubFragment : BaseFragment(), Vi } private fun createChip(activity: ActivityUiState) = activityChipOf { - text = activity.name + text = activity.activity.name isChecked = activity.isSelected setOnCheckedChangeListener { _, isChecked -> - viewModel.updateSelection(activity.id, isChecked) - } - } - - override fun onClick(view: View) { - when (view.id) { - R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() - R.id.btn_back -> (requireActivity() as OnboardingActivity).navigateToPrevPage() + viewModel.updateSelection(activity.activity.id, isChecked) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt index 612fdcb5c..1213ce8a3 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingEducationFragment.kt @@ -12,27 +12,35 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class OnboardingEducationFragment : - BaseFragment(), - View.OnClickListener { - override val layoutResId: Int = R.layout.fragment_onboarding_education + BaseFragment(R.layout.fragment_onboarding_education) { + val viewModel: OnboardingViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupDateBinding() + setupToolbar() + + observeEducations() + } + + private fun setupDateBinding() { binding.viewModel = viewModel - initClickListener() - setupEducations() + binding.onNextButtonClick = + { (requireActivity() as OnboardingActivity).navigateToNextPage() } } - private fun initClickListener() { - binding.btnNext.setOnClickListener(this) - binding.btnBack.setOnClickListener(this) + private fun setupToolbar() { + binding.tbEducationFragment.setNavigationOnClickListener { + (requireActivity() as OnboardingActivity).onBackPressedDispatcher.onBackPressed() + } } - private fun setupEducations() { - viewModel.activities.observe(viewLifecycleOwner) { activities -> + private fun observeEducations() { + viewModel.educations.observe(viewLifecycleOwner) { educations -> binding.chipgroupEduTags.removeAllViews() - activities.educations.forEach(::addEducationChip) + educations.forEach(::addEducationChip) } } @@ -41,17 +49,10 @@ class OnboardingEducationFragment : } private fun createChip(activity: ActivityUiState) = activityChipOf { - text = activity.name + text = activity.activity.name isChecked = activity.isSelected setOnCheckedChangeListener { _, isChecked -> - viewModel.updateSelection(activity.id, isChecked) - } - } - - override fun onClick(view: View) { - when (view.id) { - R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() - R.id.btn_back -> (requireActivity() as OnboardingActivity).navigateToPrevPage() + viewModel.updateSelection(activity.activity.id, isChecked) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFieldFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFieldFragment.kt index b6a03588d..579ad6e2f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFieldFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingFieldFragment.kt @@ -6,34 +6,41 @@ import androidx.fragment.app.activityViewModels import com.emmsale.R import com.emmsale.databinding.FragmentOnboardingFieldBinding import com.emmsale.presentation.base.BaseFragment -import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.views.activityChipOf import com.emmsale.presentation.ui.onboarding.uiState.ActivityUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class OnboardingFieldFragment : - BaseFragment(), - View.OnClickListener { - override val layoutResId: Int = R.layout.fragment_onboarding_field + BaseFragment(R.layout.fragment_onboarding_field) { + val viewModel: OnboardingViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + setupDataBinding() + setupToolbar() + + observeFields() + } + + private fun setupDataBinding() { binding.viewModel = viewModel - initClickListener() - setupFields() + binding.onNextButtonClick = + { (requireActivity() as OnboardingActivity).navigateToNextPage() } } - private fun initClickListener() { - binding.btnNext.setOnClickListener(this) - binding.btnBack.setOnClickListener(this) + private fun setupToolbar() { + binding.tbFieldFragment.setNavigationOnClickListener { + (requireActivity() as OnboardingActivity).onBackPressedDispatcher.onBackPressed() + } } - private fun setupFields() { - viewModel.activities.observe(viewLifecycleOwner) { activities -> + private fun observeFields() { + viewModel.fields.observe(viewLifecycleOwner) { fields -> binding.chipgroupFieldTags.removeAllViews() - activities.fields.forEach(::addFieldChip) + fields.forEach(::addFieldChip) } } @@ -42,23 +49,10 @@ class OnboardingFieldFragment : } private fun createChip(activity: ActivityUiState) = activityChipOf { - text = activity.name + text = activity.activity.name isChecked = activity.isSelected setOnCheckedChangeListener { _, isChecked -> - if (isChecked && viewModel.isExceedFieldLimit) { - showToast(R.string.onboardingfield_selection_limit_exceed) - setChecked(false) - return@setOnCheckedChangeListener - } - - viewModel.updateSelection(activity.id, isChecked) - } - } - - override fun onClick(view: View) { - when (view.id) { - R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() - R.id.btn_back -> (requireActivity() as OnboardingActivity).navigateToPrevPage() + viewModel.updateSelection(activity.activity.id, isChecked) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt index ccf073d21..26d7a5525 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingNameFragment.kt @@ -9,23 +9,20 @@ import com.emmsale.presentation.base.BaseFragment import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class OnboardingNameFragment : BaseFragment(), View.OnClickListener { - override val layoutResId: Int = R.layout.fragment_onboarding_name +class OnboardingNameFragment : + BaseFragment(R.layout.fragment_onboarding_name) { + val viewModel: OnboardingViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.viewModel = viewModel - initClickListener() - } - private fun initClickListener() { - binding.btnNext.setOnClickListener(this) + setupDateBinding() } - override fun onClick(view: View) { - when (view.id) { - R.id.btn_next -> (requireActivity() as OnboardingActivity).navigateToNextPage() - } + private fun setupDateBinding() { + binding.viewModel = viewModel + binding.onNextButtonClick = + { (requireActivity() as OnboardingActivity).navigateToNextPage() } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt index dd10c036c..e4020f6b6 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/OnboardingViewModel.kt @@ -1,22 +1,20 @@ package com.emmsale.presentation.ui.onboarding +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import androidx.lifecycle.map +import com.emmsale.data.model.ActivityType import com.emmsale.data.repository.interfaces.ActivityRepository import com.emmsale.data.repository.interfaces.ConfigRepository import com.emmsale.data.repository.interfaces.MemberRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.ui.onboarding.uiState.MemberSavingUiState -import com.emmsale.presentation.ui.onboarding.uiState.OnboardingUiState +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.onboarding.uiState.ActivityUiState +import com.emmsale.presentation.ui.onboarding.uiState.OnboardingUiEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -24,69 +22,73 @@ class OnboardingViewModel @Inject constructor( private val activityRepository: ActivityRepository, private val memberRepository: MemberRepository, private val configRepository: ConfigRepository, -) : ViewModel() { +) : RefreshableViewModel() { + val name: MutableLiveData = MutableLiveData("") - private val _activities = NotNullMutableLiveData(OnboardingUiState()) - val activities: NotNullLiveData = _activities + private val _activities = NotNullMutableLiveData(listOf()) + + val fields: LiveData> = + _activities.map { activities -> activities.filter { activity -> activity.activity.activityType == ActivityType.INTEREST_FIELD } } + + val educations: LiveData> = + _activities.map { activities -> activities.filter { activity -> activity.activity.activityType == ActivityType.EDUCATION } } - val isExceedFieldLimit: Boolean - get() = activities.value.fields.count { it.isSelected } == MAXIMUM_FIELD_SELECTION + val clubs: LiveData> = + _activities.map { activities -> activities.filter { activity -> activity.activity.activityType == ActivityType.CLUB } } + + private val _canSubmit = NotNullMutableLiveData(true) + val canSubmit: NotNullLiveData = _canSubmit + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent init { - _activities.value = _activities.value.copy(isLoading = true) fetchActivities() } - private fun fetchActivities(): Job = viewModelScope.launch { - when (val result = activityRepository.getActivities()) { - is Failure, NetworkError -> - _activities.postValue(activities.value.copy(isLoadingActivitiesFailed = true)) + private fun fetchActivities(): Job = fetchData( + fetchData = { activityRepository.getActivities() }, + onSuccess = { _activities.value = it.map(::ActivityUiState) }, + ) - is Success -> _activities.postValue(OnboardingUiState.from(result.data)) - is Unexpected -> throw Throwable(result.error) - } - } + override fun refresh(): Job = refreshData( + refresh = { activityRepository.getActivities() }, + onSuccess = { _activities.value = it.map(::ActivityUiState) }, + ) fun updateSelection(tagId: Long, isSelected: Boolean) { - val fields = _activities.value.fields - .map { if (tagId == it.id) it.copy(isSelected = isSelected) else it } - val educations = _activities.value.educations - .map { if (tagId == it.id) it.copy(isSelected = isSelected) else it } - val clubs = _activities.value.clubs - .map { if (tagId == it.id) it.copy(isSelected = isSelected) else it } - - _activities.value = _activities.value.copy( - fields = fields, - educations = educations, - clubs = clubs, - ) - } + val newActivities = _activities.value + .map { if (it.activity.id == tagId) it.copy(isSelected = isSelected) else it } - fun updateMember() { - viewModelScope.launch { - _activities.value = _activities.value.copy(isLoading = true) - when ( - val result = memberRepository.createMember( - name.value!!, - _activities.value.selectedActivityIds, - ) - ) { - is Failure, NetworkError -> updateMemberSavingUiState(MemberSavingUiState.Failed) - is Success -> { - updateMemberSavingUiState(MemberSavingUiState.Success) - configRepository.saveAutoLoginConfig(true) - } - - is Unexpected -> throw Throwable(result.error) - } + if (newActivities.isExceedFieldLimit()) { + _uiEvent.value = OnboardingUiEvent.FieldLimitExceedChecked + _activities.value = _activities.value + return } - } - private fun updateMemberSavingUiState(memberSaving: MemberSavingUiState) { - _activities.value = _activities.value.copy(memberSavingUiState = memberSaving) + _activities.value = newActivities } + private fun List.isExceedFieldLimit(): Boolean = + count { it.activity.activityType == ActivityType.INTEREST_FIELD && it.isSelected } > MAXIMUM_FIELD_SELECTION + + fun join(): Job = command( + command = { + memberRepository.createMember( + name = name.value!!, + activityIds = _activities.value.filter { it.isSelected }.map { it.activity.id }, + ) + }, + onSuccess = { + configRepository.saveAutoLoginConfig(true) + _uiEvent.value = OnboardingUiEvent.JoinComplete + }, + onFailure = { _, _ -> _uiEvent.value = OnboardingUiEvent.JoinFail }, + onStart = { _canSubmit.value = false }, + onFinish = { _canSubmit.value = true }, + ) + companion object { private const val MAXIMUM_FIELD_SELECTION = 4 } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/ActivityUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/ActivityUiState.kt index 945e0b07c..a7687f7a0 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/ActivityUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/ActivityUiState.kt @@ -3,14 +3,6 @@ package com.emmsale.presentation.ui.onboarding.uiState import com.emmsale.data.model.Activity data class ActivityUiState( - val id: Long, - val name: String, + val activity: Activity, val isSelected: Boolean = false, -) { - companion object { - fun from(activity: Activity): ActivityUiState = ActivityUiState( - id = activity.id, - name = activity.name, - ) - } -} +) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/MemberSavingUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/MemberSavingUiState.kt deleted file mode 100644 index 97d97a3c5..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/MemberSavingUiState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.emmsale.presentation.ui.onboarding.uiState - -sealed class MemberSavingUiState { - object None : MemberSavingUiState() - object Success : MemberSavingUiState() - object Failed : MemberSavingUiState() -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiEvent.kt new file mode 100644 index 000000000..ae01599fa --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiEvent.kt @@ -0,0 +1,7 @@ +package com.emmsale.presentation.ui.onboarding.uiState + +sealed interface OnboardingUiEvent { + object FieldLimitExceedChecked : OnboardingUiEvent + object JoinComplete : OnboardingUiEvent + object JoinFail : OnboardingUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiState.kt deleted file mode 100644 index a6644235a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/onboarding/uiState/OnboardingUiState.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.emmsale.presentation.ui.onboarding.uiState - -import com.emmsale.data.model.Activity -import com.emmsale.data.model.ActivityType - -data class OnboardingUiState( - val fields: List = emptyList(), - val educations: List = emptyList(), - val clubs: List = emptyList(), - val isLoading: Boolean = false, - val isLoadingActivitiesFailed: Boolean = false, - val memberSavingUiState: MemberSavingUiState = MemberSavingUiState.None, -) { - val selectedActivityIds = (fields + educations + clubs) - .filter { it.isSelected } - .map { it.id } - - companion object { - fun from(activities: List): OnboardingUiState = OnboardingUiState( - fields = activities.toUiState(ActivityType.INTEREST_FIELD), - educations = activities.toUiState(ActivityType.EDUCATION), - clubs = activities.toUiState(ActivityType.CLUB), - ) - - private fun List.toUiState(activityType: ActivityType): List = - this - .filter { it.activityType == activityType } - .map(ActivityUiState::from) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt deleted file mode 100644 index 0773663a0..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationFragment.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.ConcatAdapter -import com.emmsale.R -import com.emmsale.databinding.FragmentPrimaryNotificationBinding -import com.emmsale.presentation.base.BaseFragment -import com.emmsale.presentation.common.UiEvent -import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.ui.childCommentList.ChildCommentActivity -import com.emmsale.presentation.ui.eventDetail.EventDetailActivity -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter.PastNotificationHeaderAdapter -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter.PrimaryNotificationAdapter -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter.RecentNotificationHeaderAdapter -import com.emmsale.presentation.ui.primaryNotificationList.uiState.ChildCommentNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.InterestEventNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationScreenUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationsUiEvent -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class PrimaryNotificationFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_primary_notification - private val viewModel: PrimaryNotificationViewModel by viewModels() - - private val recentNotificationHeaderAdapter = RecentNotificationHeaderAdapter() - private val recentNotificationAdapter = PrimaryNotificationAdapter( - onNotificationClick = { notification -> - readNotification(notification.notificationId) - navigateToDetailScreen(notification) - }, - onDeleteClick = ::deleteNotification, - ) - - private val pastNotificationHeaderAdapter = PastNotificationHeaderAdapter( - onDeleteAllNotificationClick = ::showNotificationDeleteConfirmDialog, - ) - private val pastNotificationAdapter = PrimaryNotificationAdapter( - onNotificationClick = ::navigateToDetailScreen, - onDeleteClick = ::deleteNotification, - ) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - initDataBinding() - initRecyclerView() - setupUiState() - setupUiEvent() - } - - private fun initDataBinding() { - binding.viewModel = viewModel - } - - private fun showNotificationDeleteConfirmDialog() { - WarningDialog( - context = context ?: return, - title = getString(R.string.primarynotification_delete_notification_confirm_title), - message = getString(R.string.primarynotification_delete_notification_confirm_message), - positiveButtonLabel = getString(R.string.all_okay), - negativeButtonLabel = getString(R.string.all_cancel), - onPositiveButtonClick = { viewModel.deleteAllPastNotifications() }, - ).show() - } - - private fun initRecyclerView() { - val concatAdapterConfig = ConcatAdapter.Config.Builder().setIsolateViewTypes(false).build() - - binding.rvPrimarynotificationNotifications.apply { - adapter = ConcatAdapter( - concatAdapterConfig, - recentNotificationHeaderAdapter, - recentNotificationAdapter, - pastNotificationHeaderAdapter, - pastNotificationAdapter, - ) - itemAnimator = null - setHasFixedSize(false) - } - } - - private fun readNotification(notificationId: Long) { - viewModel.readNotification(notificationId) - } - - private fun navigateToDetailScreen(notification: PrimaryNotificationUiState) { - when (notification) { - is InterestEventNotificationUiState -> navigateToEventScreen(notification.eventId) - is ChildCommentNotificationUiState -> navigateToCommentScreen( - feedId = notification.feedId, - parentCommentId = notification.parentCommentId, - commentId = notification.commentId, - ) - } - } - - private fun navigateToEventScreen(eventId: Long) { - EventDetailActivity.startActivity(requireContext(), eventId) - } - - private fun navigateToCommentScreen(feedId: Long, parentCommentId: Long, commentId: Long) { - ChildCommentActivity.startActivity( - context = requireContext(), - feedId = feedId, - parentCommentId = parentCommentId, - highlightCommentId = commentId, - fromPostDetail = false, - ) - } - - private fun deleteNotification(notificationId: Long) { - viewModel.deleteNotification(notificationId) - } - - private fun setupUiState() { - viewModel.uiState.observe(viewLifecycleOwner) { uiState -> - if (uiState !is PrimaryNotificationScreenUiState.Success) return@observe - recentNotificationAdapter.submitList(uiState.recentNotifications.sortedByDescending { it.createdAt }) - pastNotificationAdapter.submitList(uiState.pastNotifications.sortedByDescending { it.createdAt }) - } - } - - private fun setupUiEvent() { - viewModel.uiEvent.observe(viewLifecycleOwner) { - handleUiEvent(it) - } - } - - private fun handleUiEvent(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - - when (content) { - PrimaryNotificationsUiEvent.NONE -> {} - PrimaryNotificationsUiEvent.DELETE_FAIL -> binding.root.showSnackBar(R.string.primarynotification_delete_notification_failed_message) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationViewModel.kt deleted file mode 100644 index 166fe8af4..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/PrimaryNotificationViewModel.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.repository.interfaces.NotificationRepository -import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.UiEvent -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationScreenUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationsUiEvent -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class PrimaryNotificationViewModel @Inject constructor( - private val tokenRepository: TokenRepository, - private val notificationRepository: NotificationRepository, -) : ViewModel(), Refreshable { - - private val _allNotificationsUiState = - NotNullMutableLiveData(PrimaryNotificationScreenUiState.Loading) - val uiState: NotNullLiveData = _allNotificationsUiState - - private val _uiEvent = NotNullMutableLiveData(UiEvent(PrimaryNotificationsUiEvent.NONE)) - val uiEvent: NotNullLiveData> = _uiEvent - - init { - refresh() - } - - override fun refresh() { - viewModelScope.launch { - val uid = tokenRepository.getToken()?.uid ?: return@launch - - when (val result = notificationRepository.getUpdatedNotifications(uid)) { - is Failure, NetworkError -> - _allNotificationsUiState.value = PrimaryNotificationScreenUiState.Error - - is Success -> - _allNotificationsUiState.value = - PrimaryNotificationScreenUiState.Success.from(result.data) - - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun deleteAllPastNotifications() { - viewModelScope.launch { - val currentUiState = _allNotificationsUiState.value - if (currentUiState !is PrimaryNotificationScreenUiState.Success) return@launch - val pastNotificationIds = currentUiState.pastNotifications.map { it.notificationId } - when ( - val result = - notificationRepository.deleteUpdatedNotifications(pastNotificationIds) - ) { - is Failure, NetworkError -> - _uiEvent.value = UiEvent(PrimaryNotificationsUiEvent.DELETE_FAIL) - - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun readNotification(notificationId: Long) { - viewModelScope.launch { - when ( - val result = - notificationRepository.updateUpdatedNotificationReadStatus(notificationId) - ) { - is Failure, NetworkError -> Unit - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } - - fun deleteNotification(notificationId: Long) { - viewModelScope.launch { - when ( - val result = - notificationRepository.deleteUpdatedNotifications(listOf(notificationId)) - ) { - is Failure, NetworkError -> - _uiEvent.value = UiEvent(PrimaryNotificationsUiEvent.DELETE_FAIL) - - is Success -> refresh() - is Unexpected -> throw Throwable(result.error) - } - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PrimaryNotificationAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PrimaryNotificationAdapter.kt deleted file mode 100644 index 0531bab81..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/adapter/PrimaryNotificationAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.adapter - -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.diffutil.PrimaryNotificationDiffUtil -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder.ChildCommentNotificationViewHolder -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder.InterestEventNotificationViewHolder -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder.PrimaryNotificationViewHolder -import com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewtype.PrimaryNotificationBodyViewType -import com.emmsale.presentation.ui.primaryNotificationList.uiState.ChildCommentNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.InterestEventNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState - -class PrimaryNotificationAdapter( - private val onNotificationClick: (notification: PrimaryNotificationUiState) -> Unit, - private val onDeleteClick: (notificationId: Long) -> Unit, -) : ListAdapter( - PrimaryNotificationDiffUtil, -) { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): PrimaryNotificationViewHolder = when (PrimaryNotificationBodyViewType.of(viewType)) { - PrimaryNotificationBodyViewType.CHILD_COMMENT -> ChildCommentNotificationViewHolder( - parent = parent, - onNotificationClick = onNotificationClick, - onDeleteNotificationClick = onDeleteClick, - ) - - PrimaryNotificationBodyViewType.INTEREST_EVENT -> InterestEventNotificationViewHolder( - parent = parent, - onNotificationClick = onNotificationClick, - onDeleteClick = onDeleteClick, - ) - } - - override fun onBindViewHolder(holder: PrimaryNotificationViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - override fun getItemViewType(position: Int): Int = when { - getItem(position) is ChildCommentNotificationUiState -> PrimaryNotificationBodyViewType.CHILD_COMMENT.viewType - getItem(position) is InterestEventNotificationUiState -> PrimaryNotificationBodyViewType.INTEREST_EVENT.viewType - else -> throw IllegalArgumentException("올바르지 않은 ViewType 입니다.") - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/diffutil/PrimaryNotificationDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/diffutil/PrimaryNotificationDiffUtil.kt deleted file mode 100644 index ff1454247..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/diffutil/PrimaryNotificationDiffUtil.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.diffutil - -import androidx.recyclerview.widget.DiffUtil -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState - -object PrimaryNotificationDiffUtil : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: PrimaryNotificationUiState, - newItem: PrimaryNotificationUiState, - ): Boolean = oldItem.notificationId == newItem.notificationId - - override fun areContentsTheSame( - oldItem: PrimaryNotificationUiState, - newItem: PrimaryNotificationUiState, - ): Boolean = oldItem == newItem -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt deleted file mode 100644 index c95c2606a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/InterestEventNotificationViewHolder.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.emmsale.R -import com.emmsale.databinding.ItemPrimaryNotificationBinding -import com.emmsale.presentation.ui.primaryNotificationList.uiState.InterestEventNotificationUiState -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState - -class InterestEventNotificationViewHolder( - parent: ViewGroup, - onNotificationClick: (notification: PrimaryNotificationUiState) -> Unit = {}, - onDeleteClick: (notificationId: Long) -> Unit = {}, -) : PrimaryNotificationViewHolder( - LayoutInflater.from(parent.context).inflate( - R.layout.item_primary_notification, - parent, - false, - ), -) { - private val binding = ItemPrimaryNotificationBinding.bind(itemView) - - init { - binding.onNotificationClick = onNotificationClick - binding.onDeleteClick = onDeleteClick - } - - override fun bind(item: PrimaryNotificationUiState) { - if (item !is InterestEventNotificationUiState) return - - binding.interestEventNotification = item - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PrimaryNotificationViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PrimaryNotificationViewHolder.kt deleted file mode 100644 index 66f559a26..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/recyclerView/viewHolder/PrimaryNotificationViewHolder.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.recyclerView.viewHolder - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.emmsale.presentation.ui.primaryNotificationList.uiState.PrimaryNotificationUiState - -abstract class PrimaryNotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) { - abstract fun bind(item: PrimaryNotificationUiState) -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/ChildCommentNotificationUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/ChildCommentNotificationUiState.kt deleted file mode 100644 index 969bb665c..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/ChildCommentNotificationUiState.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.uiState - -import com.emmsale.data.model.updatedNotification.ChildCommentNotification -import java.time.LocalDateTime - -class ChildCommentNotificationUiState( - notificationId: Long, - receiverId: Long, - createdAt: LocalDateTime, - isRead: Boolean, - val commentId: Long, - val commentContent: String, - val parentCommentId: Long, - val feedId: Long, - val commenterProfileImageUrl: String, -) : PrimaryNotificationUiState( - notificationId = notificationId, - receiverId = receiverId, - createdAt = createdAt, - isRead = isRead, -) { - override fun equals(other: Any?): Boolean { - if (other !is ChildCommentNotificationUiState) return false - if (!super.equals(other)) return false - - return commentId == other.commentId && - commentContent == other.commentContent && - parentCommentId == other.parentCommentId && - feedId == other.feedId && - commenterProfileImageUrl == other.commenterProfileImageUrl - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = PRIME * result + commentId.hashCode() - result = PRIME * result + commentContent.hashCode() - result = PRIME * result + parentCommentId.hashCode() - result = PRIME * result + feedId.hashCode() - result = PRIME * result + commenterProfileImageUrl.hashCode() - return result - } - - companion object { - fun from(notification: ChildCommentNotification) = ChildCommentNotificationUiState( - notificationId = notification.id, - receiverId = notification.receiverId, - createdAt = notification.createdAt, - isRead = notification.isRead, - commentId = notification.childCommentId, - commentContent = notification.childCommentContent, - parentCommentId = notification.parentCommentId, - feedId = notification.feedId, - commenterProfileImageUrl = notification.commentProfileImageUrl, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/InterestEventNotificationUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/InterestEventNotificationUiState.kt deleted file mode 100644 index 0851c49d0..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/InterestEventNotificationUiState.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.uiState - -import com.emmsale.data.model.updatedNotification.InterestEventNotification -import java.time.LocalDateTime - -class InterestEventNotificationUiState( - notificationId: Long, - receiverId: Long, - createdAt: LocalDateTime, - isRead: Boolean, - val eventId: Long, - val eventTitle: String, -) : PrimaryNotificationUiState( - notificationId = notificationId, - receiverId = receiverId, - createdAt = createdAt, - isRead = isRead, -) { - - override fun equals(other: Any?): Boolean { - if (other !is InterestEventNotificationUiState) return false - if (!super.equals(other)) return false - - return eventId == other.eventId - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = PRIME * result + eventId.hashCode() - return result - } - - companion object { - fun from(notification: InterestEventNotification) = InterestEventNotificationUiState( - notificationId = notification.id, - receiverId = notification.receiverId, - createdAt = notification.createdAt, - isRead = notification.isRead, - eventId = notification.eventId, - eventTitle = notification.eventTitle, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationScreenUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationScreenUiState.kt deleted file mode 100644 index a3922a8cf..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationScreenUiState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.uiState - -import com.emmsale.data.model.updatedNotification.UpdatedNotification - -sealed interface PrimaryNotificationScreenUiState { - data class Success( - val recentNotifications: List, - val pastNotifications: List, - ) : PrimaryNotificationScreenUiState { - companion object { - fun from(notifications: List) = Success( - recentNotifications = notifications.filterNot { it.isRead } - .map(PrimaryNotificationUiState.Companion::from), - pastNotifications = notifications.filter { it.isRead } - .map(PrimaryNotificationUiState.Companion::from), - ) - } - } - - object Loading : PrimaryNotificationScreenUiState - - object Error : PrimaryNotificationScreenUiState -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationUiState.kt deleted file mode 100644 index 03452e2ad..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationUiState.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.uiState - -import com.emmsale.data.model.updatedNotification.ChildCommentNotification -import com.emmsale.data.model.updatedNotification.InterestEventNotification -import com.emmsale.data.model.updatedNotification.UpdatedNotification -import java.time.LocalDateTime - -abstract class PrimaryNotificationUiState( - val notificationId: Long, - val receiverId: Long, - val createdAt: LocalDateTime, - val isRead: Boolean, -) { - override fun equals(other: Any?): Boolean { - if (other !is PrimaryNotificationUiState) return false - return notificationId == other.notificationId && - receiverId == other.receiverId && - createdAt == other.createdAt && - isRead == other.isRead - } - - override fun hashCode(): Int { - var result = 1 - result = PRIME * result + notificationId.hashCode() - result = PRIME * result + receiverId.hashCode() - result = PRIME * result + createdAt.hashCode() - result = PRIME * result + isRead.hashCode() - return result - } - - companion object { - @JvmStatic - protected val PRIME = 31 - - fun from(notification: UpdatedNotification): PrimaryNotificationUiState = - when (notification) { - is ChildCommentNotification -> ChildCommentNotificationUiState.from(notification) - is InterestEventNotification -> InterestEventNotificationUiState.from(notification) - else -> throw IllegalArgumentException("${PrimaryNotificationUiState::class.simpleName} 타입이 아닙니다.") - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationsUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationsUiEvent.kt deleted file mode 100644 index 9ce53fe30..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/primaryNotificationList/uiState/PrimaryNotificationsUiEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.emmsale.presentation.ui.primaryNotificationList.uiState - -enum class PrimaryNotificationsUiEvent { - NONE, DELETE_FAIL, -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileActivity.kt index d983bbb2b..84d9f8ecf 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileActivity.kt @@ -4,29 +4,27 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R import com.emmsale.databinding.ActivityProfileBinding -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.dp import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.extension.showToast +import com.emmsale.presentation.common.recyclerView.IntervalItemDecoration import com.emmsale.presentation.common.views.CategoryTagChip import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog -import com.emmsale.presentation.ui.login.LoginActivity import com.emmsale.presentation.ui.messageList.MessageListActivity import com.emmsale.presentation.ui.profile.ProfileViewModel.Companion.KEY_MEMBER_ID import com.emmsale.presentation.ui.profile.recyclerView.ActivitiesAdapter -import com.emmsale.presentation.ui.profile.recyclerView.ActivitiesAdapterDecoration import com.emmsale.presentation.ui.profile.uiState.ProfileUiEvent import com.emmsale.presentation.ui.profile.uiState.ProfileUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ProfileActivity : AppCompatActivity() { - private val binding by lazy { ActivityProfileBinding.inflate(layoutInflater) } - private val viewModel: ProfileViewModel by viewModels() +class ProfileActivity : NetworkActivity(R.layout.activity_profile) { + + override val viewModel: ProfileViewModel by viewModels() private val menuDialog: BottomMenuDialog by lazy { BottomMenuDialog(this) } @@ -36,19 +34,20 @@ class ProfileActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(binding.root) - initDataBinding() - initToolbar() - setupUiLogic() - initActivitiesRecyclerView() + setupDataBinding() + setupToolbar() + setupActivitiesRecyclerView() + + observeProfile() + observeUiEvent() } - private fun initDataBinding() { - binding.lifecycleOwner = this + private fun setupDataBinding() { binding.viewModel = viewModel } - private fun initToolbar() { - binding.tbProfileToolbar.setNavigationOnClickListener { finish() } + private fun setupToolbar() { + binding.tbProfileToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } binding.tbProfileToolbar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.more -> showMoreMenu() @@ -62,7 +61,7 @@ class ProfileActivity : AppCompatActivity() { menuDialog.apply { addMenuItemBelow("쪽지 보내기") { onSendMessageButtonClick() } if (viewModel.isBlocked()) { - addMenuItemBelow(getString(R.string.profilemenudialog_unblock_button_label)) { onUnblockButtonClick() } + addMenuItemBelow(getString(R.string.profilemenudialog_unblock_button_label)) { viewModel.unblockMember() } } else { addMenuItemBelow(getString(R.string.profilemenudialog_block_button_label)) { onBlockButtonClick() } } @@ -84,23 +83,21 @@ class ProfileActivity : AppCompatActivity() { ).show() } - private fun onUnblockButtonClick() { - viewModel.unblockMember() - } - - private fun setupUiLogic() { - setupLoginUiLogic() - setupProfileUiLogic() - setupProfileEventUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(this) { - handleNotLogin(it) + private fun setupActivitiesRecyclerView() { + val decoration = IntervalItemDecoration(height = 13.dp) + listOf( + binding.rvProfileEducations, + binding.rvProfileClubs, + ).forEach { + it.apply { + adapter = ActivitiesAdapter() + itemAnimator = null + addItemDecoration(decoration) + } } } - private fun setupProfileUiLogic() { + private fun observeProfile() { viewModel.profile.observe(this) { handleLoginMember(it) handleFields(it) @@ -108,52 +105,12 @@ class ProfileActivity : AppCompatActivity() { } } - private fun setupProfileEventUiLogic() { - viewModel.uiEvent.observe(this) { - handleUiEvent(it) - } - } - - private fun handleUiEvent(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content) { - ProfileUiEvent.BlockComplete -> InfoDialog( - context = this, - title = getString(R.string.profile_block_complete_dialog_title), - message = getString(R.string.profile_block_complete_dialog_message), - ).show() - - ProfileUiEvent.BlockFail -> binding.root.showSnackBar(getString(R.string.profile_block_fail_message)) - is ProfileUiEvent.MessageSendComplete -> { - MessageListActivity.startActivity( - this, - content.roomId, - content.otherId, - ) - sendMessageDialog.dismiss() - } - - ProfileUiEvent.MessageSendFail -> binding.root.showSnackBar(getString(R.string.sendmessagedialog_message_send_fail_message)) - ProfileUiEvent.None -> {} - ProfileUiEvent.UnblockFail -> binding.root.showSnackBar(getString(R.string.profile_unblock_fail_message)) - ProfileUiEvent.UnblockSuccess -> binding.root.showSnackBar(getString(R.string.profile_unblock_complete_message)) - is ProfileUiEvent.UnexpectedError -> showToast(content.errorMessage) - } - } - private fun handleLoginMember(profile: ProfileUiState) { if (profile.isLoginMember) { binding.tbProfileToolbar.menu.clear() } } - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(this) - finish() - } - } - private fun handleFields(profile: ProfileUiState) { binding.cgProfileFields.removeAllViews() @@ -173,17 +130,32 @@ class ProfileActivity : AppCompatActivity() { ) } - private fun initActivitiesRecyclerView() { - val decoration = ActivitiesAdapterDecoration() - listOf( - binding.rvProfileEducations, - binding.rvProfileClubs, - ).forEach { - it.apply { - adapter = ActivitiesAdapter() - itemAnimator = null - addItemDecoration(decoration) + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: ProfileUiEvent) { + when (uiEvent) { + ProfileUiEvent.BlockComplete -> InfoDialog( + context = this, + title = getString(R.string.profile_block_complete_dialog_title), + message = getString(R.string.profile_block_complete_dialog_message), + ).show() + + ProfileUiEvent.BlockFail -> binding.root.showSnackBar(getString(R.string.profile_block_fail_message)) + is ProfileUiEvent.MessageSendComplete -> { + MessageListActivity.startActivity( + this, + uiEvent.roomId, + uiEvent.otherId, + ) + sendMessageDialog.clearText() + sendMessageDialog.dismiss() } + + ProfileUiEvent.MessageSendFail -> binding.root.showSnackBar(getString(R.string.sendmessagedialog_message_send_fail_message)) + ProfileUiEvent.UnblockFail -> binding.root.showSnackBar(getString(R.string.profile_unblock_fail_message)) + ProfileUiEvent.UnblockSuccess -> binding.root.showSnackBar(getString(R.string.profile_unblock_complete_message)) } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileViewModel.kt index c941cee43..fc64550ab 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/ProfileViewModel.kt @@ -1,24 +1,25 @@ package com.emmsale.presentation.ui.profile +import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.BlockedMember +import com.emmsale.data.model.Member import com.emmsale.data.repository.interfaces.BlockedMemberRepository import com.emmsale.data.repository.interfaces.MemberRepository import com.emmsale.data.repository.interfaces.MessageRoomRepository import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.UiEvent +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.ScreenUiState import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.profile.uiState.BlockedMemberUiState +import com.emmsale.presentation.common.livedata.SingleLiveEvent import com.emmsale.presentation.ui.profile.uiState.ProfileUiEvent import com.emmsale.presentation.ui.profile.uiState.ProfileUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,132 +30,78 @@ class ProfileViewModel @Inject constructor( private val memberRepository: MemberRepository, private val messageRoomRepository: MessageRoomRepository, private val blockedMemberRepository: BlockedMemberRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { private val memberId: Long = requireNotNull(savedStateHandle[KEY_MEMBER_ID]) { "[ERROR] 멤버 아이디를 가져오지 못했어요." } private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin - - private val _profile = NotNullMutableLiveData(ProfileUiState.FIRST_LOADING) + private val _profile = NotNullMutableLiveData(ProfileUiState(false, Member())) val profile: NotNullLiveData = _profile - private val _blockedMembers = NotNullMutableLiveData(listOf()) - - private val _uiEvent: NotNullMutableLiveData> = - NotNullMutableLiveData(UiEvent(ProfileUiEvent.None)) - val uiEvent: NotNullLiveData> = _uiEvent - - init { - refresh() - } - - override fun refresh() { - viewModelScope.launch { - _profile.value = _profile.value.changeToLoadingState() - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - - when (val result = memberRepository.getMember(memberId)) { - is Failure, NetworkError -> - _profile.value = _profile.value.changeToFetchingErrorState() - - is Success -> - _profile.value = _profile.value.changeMemberState(result.data, token.uid) + private val _blockedMembers = NotNullMutableLiveData(listOf()) - is Unexpected -> throw Throwable(result.error) - } + private val _canSendMessage = NotNullMutableLiveData(true) + val canSendMessage: NotNullLiveData = _canSendMessage - fetchBlockedMembers() + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - _profile.value = _profile.value.copy(isLoading = false) - } - } - - private fun fetchBlockedMembers() { - viewModelScope.launch { - when (val result = blockedMemberRepository.getBlockedMembers()) { - is Failure, NetworkError -> - _profile.value = _profile.value.changeToFetchingErrorState() - - is Success -> - _blockedMembers.value = result.data.map { BlockedMemberUiState.from(it) } - - is Unexpected -> throw Throwable(result.error) - } - } + init { + fetchAll() } - fun isBlocked(): Boolean { - return memberId in _blockedMembers.value.map { it.blockedMemberId } + private fun fetchAll(): Job = viewModelScope.launch { + changeToLoadingState() + refresh() } - fun blockMember() { - viewModelScope.launch { - _profile.value = _profile.value.copy(isLoading = true) - when (val result = memberRepository.blockMember(memberId)) { - is Failure -> _uiEvent.value = UiEvent(ProfileUiEvent.BlockFail) - - NetworkError -> _profile.value = _profile.value.copy(isError = true) - is Success -> { - _uiEvent.value = UiEvent(ProfileUiEvent.BlockComplete) - refresh() - } - - is Unexpected -> - _uiEvent.value = UiEvent(ProfileUiEvent.UnexpectedError(result.error.toString())) - } - _profile.value = _profile.value.copy(isLoading = false) - } + override fun refresh(): Job = viewModelScope.launch { + listOf(fetchProfile(), fetchBlockedMembers()).joinAll() + if (_screenUiState.value == ScreenUiState.NETWORK_ERROR) return@launch + _screenUiState.value = ScreenUiState.NONE } - fun unblockMember() { - viewModelScope.launch { - _profile.value = _profile.value.copy(isLoading = true) + private fun fetchProfile(): Job = fetchData( + fetchData = { memberRepository.getMember(memberId) }, + onSuccess = { _profile.value = ProfileUiState(it, uid) }, + onLoading = {}, + ) + + private fun fetchBlockedMembers(): Job = fetchData( + fetchData = { blockedMemberRepository.getBlockedMembers() }, + onSuccess = { _blockedMembers.value = it }, + onLoading = {}, + onNetworkError = {}, + ) + + fun isBlocked(): Boolean = memberId in _blockedMembers.value.map { it.blockedMemberId } + + fun blockMember(): Job = commandAndRefresh( + command = { memberRepository.blockMember(memberId) }, + onSuccess = { _uiEvent.value = ProfileUiEvent.BlockComplete }, + onFailure = { _, _ -> _uiEvent.value = ProfileUiEvent.BlockFail }, + ) + + fun unblockMember(): Job = commandAndRefresh( + command = { val blockId = _blockedMembers.value.find { it.blockedMemberId == memberId }?.blockId - ?: return@launch - when (val result = blockedMemberRepository.deleteBlockedMember(blockId)) { - is Failure -> _uiEvent.value = UiEvent(ProfileUiEvent.UnblockFail) - NetworkError -> _profile.value = _profile.value.copy(isError = true) - is Success -> { - _uiEvent.value = UiEvent(ProfileUiEvent.UnblockSuccess) - refresh() - } - - is Unexpected -> - _uiEvent.value = UiEvent(ProfileUiEvent.UnexpectedError(result.error.toString())) - } - _profile.value = _profile.value.copy(isLoading = false) - } - } - - fun sendMessage(message: String) { - viewModelScope.launch { - _profile.value = _profile.value.copy(isLoading = true) - when ( - val result = - messageRoomRepository.sendMessage(uid, _profile.value.member.id, message) - ) { - is Failure -> - _uiEvent.value = UiEvent(ProfileUiEvent.MessageSendFail) - - NetworkError -> _profile.value = _profile.value.copy(isError = true) - is Success -> - _uiEvent.value = UiEvent( - ProfileUiEvent.MessageSendComplete(result.data, _profile.value.member.id), - ) - - is Unexpected -> - _uiEvent.value = UiEvent(ProfileUiEvent.UnexpectedError(result.error.toString())) - } - _profile.value = _profile.value.copy(isLoading = false) - } - } + ?: return@commandAndRefresh Failure(-1, null) + blockedMemberRepository.deleteBlockedMember(blockId) + }, + onSuccess = { _uiEvent.value = ProfileUiEvent.UnblockSuccess }, + onFailure = { _, _ -> _uiEvent.value = ProfileUiEvent.UnblockFail }, + ) + + fun sendMessage(message: String): Job = command( + command = { messageRoomRepository.sendMessage(uid, _profile.value.member.id, message) }, + onSuccess = { + _uiEvent.value = ProfileUiEvent.MessageSendComplete(it, _profile.value.member.id) + }, + onFailure = { _, _ -> _uiEvent.value = ProfileUiEvent.MessageSendFail }, + onStart = { _canSendMessage.value = false }, + onFinish = { _canSendMessage.value = true }, + ) companion object { const val KEY_MEMBER_ID = "KEY_MEMBER_ID" diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/SendMessageDialog.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/SendMessageDialog.kt index d94edd248..0366c43b0 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/SendMessageDialog.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/SendMessageDialog.kt @@ -40,19 +40,14 @@ class SendMessageDialog : DialogFragment() { private fun setupDataBinding() { binding.lifecycleOwner = viewLifecycleOwner binding.vm = viewModel - binding.onSendButtonClick = ::onSendButtonClick - binding.onCancelButtonClick = ::onCancelButtonClick + binding.onSendButtonClick = { viewModel.sendMessage(it) } + binding.onCancelButtonClick = { dismiss() } } - private fun onSendButtonClick(message: String) { - viewModel.sendMessage(message) + fun clearText() { binding.etSendmessagedialogMessage.text.clear() } - private fun onCancelButtonClick() { - dismiss() - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ActivityUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ActivityUiState.kt deleted file mode 100644 index b2141e36a..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ActivityUiState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.emmsale.presentation.ui.profile.uiState - -import com.emmsale.data.model.Activity - -data class ActivityUiState( - val id: Long, - val name: String, -) { - - companion object { - fun from(activity: Activity): ActivityUiState = - ActivityUiState( - id = activity.id, - name = activity.name, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/BlockedMemberUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/BlockedMemberUiState.kt deleted file mode 100644 index 7a6fda5ec..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/BlockedMemberUiState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.emmsale.presentation.ui.profile.uiState - -import com.emmsale.data.model.BlockedMember - -data class BlockedMemberUiState( - val blockedMemberId: Long, - val blockId: Long, -) { - companion object { - fun from(blockedMember: BlockedMember) = BlockedMemberUiState( - blockedMemberId = blockedMember.id, - blockId = blockedMember.blockId, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiEvent.kt index d06e7ca43..da937521a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiEvent.kt @@ -1,12 +1,10 @@ package com.emmsale.presentation.ui.profile.uiState sealed interface ProfileUiEvent { - data class UnexpectedError(val errorMessage: String) : ProfileUiEvent object BlockFail : ProfileUiEvent object BlockComplete : ProfileUiEvent object UnblockFail : ProfileUiEvent object UnblockSuccess : ProfileUiEvent object MessageSendFail : ProfileUiEvent data class MessageSendComplete(val roomId: String, val otherId: Long) : ProfileUiEvent - object None : ProfileUiEvent } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiState.kt index 0df805995..4efb9e223 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiState.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/profile/uiState/ProfileUiState.kt @@ -1,43 +1,10 @@ package com.emmsale.presentation.ui.profile.uiState -import com.emmsale.data.model.Activity import com.emmsale.data.model.Member data class ProfileUiState( - val isLoading: Boolean, - val isError: Boolean, val isLoginMember: Boolean, val member: Member = Member(), ) { - - fun changeToLoadingState(): ProfileUiState = copy( - isLoading = true, - isError = false, - ) - - fun changeToFetchingErrorState(): ProfileUiState = copy( - isLoading = false, - isError = true, - ) - - fun changeMemberState(member: Member, loginMemberId: Long): ProfileUiState = copy( - isLoading = false, - isError = false, - isLoginMember = member.id == loginMemberId, - member = member, - ) - - fun changeActivityState(activities: List): ProfileUiState = copy( - isLoading = false, - isError = false, - member = member.copy(activities = activities), - ) - - companion object { - val FIRST_LOADING = ProfileUiState( - isLoading = true, - isError = false, - isLoginMember = false, - ) - } + constructor(member: Member, uid: Long) : this(uid == member.id, member) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailActivity.kt similarity index 56% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailActivity.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailActivity.kt index 0f8634c87..260af3012 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailActivity.kt @@ -6,33 +6,28 @@ import android.os.Bundle import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity import com.emmsale.R -import com.emmsale.databinding.ActivityRecruitmentPostDetailBinding -import com.emmsale.presentation.common.UiEvent +import com.emmsale.databinding.ActivityRecruitmentDetailBinding +import com.emmsale.presentation.base.NetworkActivity import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.views.InfoDialog import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.common.views.bottomMenuDialog.BottomMenuDialog import com.emmsale.presentation.common.views.bottomMenuDialog.MenuItemType -import com.emmsale.presentation.ui.eventDetail.EventDetailActivity import com.emmsale.presentation.ui.messageList.MessageListActivity import com.emmsale.presentation.ui.profile.ProfileActivity -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentPostDetailViewModel.Companion.EVENT_ID_KEY -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentPostDetailViewModel.Companion.RECRUITMENT_ID_KEY +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailViewModel.Companion.EVENT_ID_KEY +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailViewModel.Companion.RECRUITMENT_ID_KEY import com.emmsale.presentation.ui.recruitmentDetail.uiState.RecruitmentPostDetailUiEvent -import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingActivity +import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentWritingActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class RecruitmentPostDetailActivity : AppCompatActivity() { - private val binding by lazy { ActivityRecruitmentPostDetailBinding.inflate(layoutInflater) } - private val viewModel: RecruitmentPostDetailViewModel by viewModels() +class RecruitmentDetailActivity : + NetworkActivity(R.layout.activity_recruitment_detail) { + + override val viewModel: RecruitmentDetailViewModel by viewModels() - private val isNavigatedFromMyPost: Boolean by lazy { - intent.getBooleanExtra(FROM_MY_POST_KEY, false) - } private val postEditorDialog: BottomMenuDialog by lazy { BottomMenuDialog(this).apply { addMenuItemBelow( @@ -50,13 +45,22 @@ class RecruitmentPostDetailActivity : AppCompatActivity() { private val sendMessageDialog: SendMessageDialog by lazy { SendMessageDialog() } - private val fetchByResultActivityLauncher = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result -> - if (result == null || result.resultCode != RESULT_OK) return@registerForActivityResult - viewModel.refresh() - } + private val fetchByResultActivityLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result == null || result.resultCode != RESULT_OK) return@registerForActivityResult + viewModel.refresh() + } + + private fun navigateToEditPage() { + val intent = RecruitmentWritingActivity.getEditModeIntent( + context = this, + eventId = viewModel.eventId, + recruitmentId = viewModel.recruitmentId, + recruitmentContent = viewModel.recruitment.value.recruitment.content, + ) + fetchByResultActivityLauncher.launch(intent) + } private fun showDeleteDialog() { WarningDialog( @@ -65,7 +69,7 @@ class RecruitmentPostDetailActivity : AppCompatActivity() { message = getString(R.string.recruitmentpostdetail_delete_dialog_message), positiveButtonLabel = getString(R.string.all_delete_button_label), negativeButtonLabel = getString(R.string.all_cancel), - onPositiveButtonClick = { viewModel.deleteRecruitmentPost() }, + onPositiveButtonClick = { viewModel.deleteRecruitment() }, ).show() } @@ -92,42 +96,34 @@ class RecruitmentPostDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initBinding() - initClickListener() - setupEventUiLogic() - } - - private fun initBinding() { setContentView(binding.root) - binding.lifecycleOwner = this - binding.vm = viewModel - binding.isNavigatedFromMyPost = isNavigatedFromMyPost - } - private fun initClickListener() { - setUpOptionButtonClick() - setUpRequestCompanionButtonClick() - setUpBackPressButtonClick() - setUpBackPressIconClick() - setUpProfileClick() - setUpNavigateToEventDetailButtonClick() + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + + observeUiEvent() } - private fun setupEventUiLogic() { - viewModel.uiEvent.observe(this) { - handleUiEvent(it) - } + private fun setupDataBinding() { + binding.vm = viewModel + binding.onRequestRecruitmentButtonClick = + { sendMessageDialog.show(supportFragmentManager, SendMessageDialog.TAG) } + binding.onProfileImageClick = { memberId -> ProfileActivity.startActivity(this, memberId) } } - private fun setUpRequestCompanionButtonClick() { - binding.btnRecruitmentdetailRequestCompanion.setOnClickListener { - sendMessageDialog.show(supportFragmentManager, SendMessageDialog.TAG) + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback(this) { + setResult(RESULT_OK) + finish() } } - private fun setUpOptionButtonClick() { + private fun setupToolbar() { + binding.tbToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + binding.tbToolbar.setOnMenuItemClickListener { - if (viewModel.recruitmentPost.value.isMyPost) { + if (viewModel.recruitment.value.isMyPost) { postEditorDialog.show() } else { postReportDialog.show() @@ -136,49 +132,24 @@ class RecruitmentPostDetailActivity : AppCompatActivity() { } } - private fun setUpNavigateToEventDetailButtonClick() { - binding.btnRecruitmentdetailNavigateToEventDetail.setOnClickListener { - EventDetailActivity.startActivity(this, viewModel.eventId) - } - } - - private fun setUpBackPressButtonClick() { - onBackPressedDispatcher.addCallback(this) { - finishWithResult() - } - } - - private fun setUpBackPressIconClick() { - binding.tbToolbar.setNavigationOnClickListener { - finishWithResult() - } - } - - private fun setUpProfileClick() { - binding.ivRecruitmentdetailProfileImage.setOnClickListener { - ProfileActivity.startActivity(this, viewModel.recruitmentPost.value.memberId) - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) } - private fun handleUiEvent(event: UiEvent) { - val content = event.getContentIfNotHandled() ?: return - when (content) { + private fun handleUiEvent(uiEvent: RecruitmentPostDetailUiEvent) { + when (uiEvent) { is RecruitmentPostDetailUiEvent.MessageSendComplete -> { MessageListActivity.startActivity( this, - content.roomId, - content.otherId, + uiEvent.roomId, + uiEvent.otherId, ) + sendMessageDialog.clearText() sendMessageDialog.dismiss() } RecruitmentPostDetailUiEvent.MessageSendFail -> binding.root.showSnackBar(R.string.sendmessagedialog_message_send_fail_message) - RecruitmentPostDetailUiEvent.None -> {} - RecruitmentPostDetailUiEvent.PostDeleteComplete -> { - binding.root.showSnackBar(getString(R.string.recruitmentpostdetail_deletion_success_message)) - onBackPressedDispatcher.onBackPressed() - } - + RecruitmentPostDetailUiEvent.PostDeleteComplete -> onBackPressedDispatcher.onBackPressed() RecruitmentPostDetailUiEvent.PostDeleteFail -> binding.root.showSnackBar(getString(R.string.recruitmentpostdetail_deletion_fail_message)) RecruitmentPostDetailUiEvent.PostFetchFail -> binding.root.showSnackBar(getString(R.string.recruitmentpostdetail_fail_request_message)) RecruitmentPostDetailUiEvent.ReportComplete -> InfoDialog( @@ -196,39 +167,19 @@ class RecruitmentPostDetailActivity : AppCompatActivity() { ).show() RecruitmentPostDetailUiEvent.ReportFail -> binding.root.showSnackBar(getString(R.string.all_report_fail_message)) - is RecruitmentPostDetailUiEvent.UnexpectedError -> showToast(content.errorMessage) } } - private fun finishWithResult() { - setResult(RESULT_OK) - finish() - } - - private fun navigateToEditPage() { - val intent = RecruitmentPostWritingActivity.getEditModeIntent( - this, - viewModel.eventId, - viewModel.recruitmentId, - viewModel.recruitmentPost.value.content, - ) - fetchByResultActivityLauncher.launch(intent) - } - companion object { - private const val FROM_MY_POST_KEY = "FROM_MY_POST_KEY" - - fun getIntent( + fun startActivity( context: Context, eventId: Long, recruitmentId: Long, - isNavigatedFromMyPost: Boolean = false, - ): Intent { - val intent = Intent(context, RecruitmentPostDetailActivity::class.java) - intent.putExtra(EVENT_ID_KEY, eventId) - intent.putExtra(RECRUITMENT_ID_KEY, recruitmentId) - intent.putExtra(FROM_MY_POST_KEY, isNavigatedFromMyPost) - return intent + ) { + Intent(context, RecruitmentDetailActivity::class.java) + .putExtra(EVENT_ID_KEY, eventId) + .putExtra(RECRUITMENT_ID_KEY, recruitmentId) + .run { context.startActivity(this) } } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailViewModel.kt new file mode 100644 index 000000000..51548eae5 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentDetailViewModel.kt @@ -0,0 +1,114 @@ +package com.emmsale.presentation.ui.recruitmentDetail + +import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle +import com.emmsale.data.repository.interfaces.MessageRoomRepository +import com.emmsale.data.repository.interfaces.RecruitmentRepository +import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.recruitmentDetail.uiState.RecruitmentPostDetailUiEvent +import com.emmsale.presentation.ui.recruitmentDetail.uiState.RecruitmentUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject + +@HiltViewModel +class RecruitmentDetailViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val recruitmentRepository: RecruitmentRepository, + private val messageRoomRepository: MessageRoomRepository, + tokenRepository: TokenRepository, +) : RefreshableViewModel() { + val eventId: Long = requireNotNull(savedStateHandle[EVENT_ID_KEY]) { + "[ERROR] 행사 아이디를 가져오지 못했어요." + } + val recruitmentId: Long = requireNotNull(savedStateHandle[RECRUITMENT_ID_KEY]) { + "[ERROR] 모집글 아이디를 가져오지 못했어요." + } + + private val _recruitment: NotNullMutableLiveData = + NotNullMutableLiveData(RecruitmentUiState()) + val recruitment: NotNullLiveData = _recruitment + + private val _canSendMessage = NotNullMutableLiveData(true) + val canSendMessage: NotNullLiveData = _canSendMessage + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent + + private val myUid = requireNotNull(tokenRepository.getMyUid()) { + "[ERROR] 내 아이디를 가져오지 못했어요." + } + + init { + fetchRecruitment() + } + + private fun fetchRecruitment(): Job = fetchData( + fetchData = { recruitmentRepository.getEventRecruitment(eventId, recruitmentId) }, + onSuccess = { _recruitment.value = RecruitmentUiState(it, myUid) }, + onFailure = { _, _ -> _uiEvent.value = RecruitmentPostDetailUiEvent.PostFetchFail }, + ) + + override fun refresh(): Job = refreshData( + refresh = { recruitmentRepository.getEventRecruitment(eventId, recruitmentId) }, + onSuccess = { _recruitment.value = RecruitmentUiState(it, myUid) }, + ) + + fun deleteRecruitment(): Job = command( + command = { recruitmentRepository.deleteRecruitment(eventId, recruitmentId) }, + onSuccess = { _uiEvent.value = RecruitmentPostDetailUiEvent.PostDeleteComplete }, + onFailure = { _, _ -> + _uiEvent.value = RecruitmentPostDetailUiEvent.PostDeleteFail + }, + ) + + fun reportRecruitment(): Job = command( + command = { + recruitmentRepository.reportRecruitment( + recruitmentId = _recruitment.value.recruitment.id, + authorId = _recruitment.value.recruitment.writer.id, + reporterId = myUid, + ) + }, + onSuccess = { _uiEvent.value = RecruitmentPostDetailUiEvent.ReportComplete }, + onFailure = { code, _ -> + if (code == REPORT_DUPLICATE_ERROR_CODE) { + _uiEvent.value = RecruitmentPostDetailUiEvent.ReportDuplicate + } else { + _uiEvent.value = RecruitmentPostDetailUiEvent.ReportFail + } + }, + ) + + fun sendMessage(message: String): Job = command( + command = { + messageRoomRepository.sendMessage( + senderId = myUid, + receiverId = _recruitment.value.recruitment.writer.id, + message = message, + ) + }, + onSuccess = { + _uiEvent.value = RecruitmentPostDetailUiEvent.MessageSendComplete( + roomId = it, + otherId = _recruitment.value.recruitment.writer.id, + ) + }, + onFailure = { _, _ -> + _uiEvent.value = RecruitmentPostDetailUiEvent.MessageSendFail + }, + onStart = { _canSendMessage.value = false }, + onFinish = { _canSendMessage.value = true }, + ) + + companion object { + const val EVENT_ID_KEY = "EVENT_ID_KEY" + const val RECRUITMENT_ID_KEY = "RECRUITMENT_ID_KEY" + + private const val REPORT_DUPLICATE_ERROR_CODE = 400 + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailViewModel.kt deleted file mode 100644 index 5f8ff7c82..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/RecruitmentPostDetailViewModel.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentDetail - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.repository.interfaces.MessageRoomRepository -import com.emmsale.data.repository.interfaces.RecruitmentRepository -import com.emmsale.data.repository.interfaces.TokenRepository -import com.emmsale.presentation.common.UiEvent -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.recruitmentDetail.uiState.RecruitmentPostDetailUiEvent -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class RecruitmentPostDetailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val recruitmentRepository: RecruitmentRepository, - private val messageRoomRepository: MessageRoomRepository, - tokenRepository: TokenRepository, -) : ViewModel(), Refreshable { - val eventId: Long = requireNotNull(savedStateHandle[EVENT_ID_KEY]) { - "[ERROR] 행사 아이디를 가져오지 못했어요." - } - val recruitmentId: Long = requireNotNull(savedStateHandle[RECRUITMENT_ID_KEY]) { - "[ERROR] 모집글 아이디를 가져오지 못했어요." - } - - private val _recruitmentPost: NotNullMutableLiveData = - NotNullMutableLiveData(RecruitmentPostUiState.Loading) - val recruitmentPost: NotNullLiveData = _recruitmentPost - - private val _isAlreadyRequest: MutableLiveData = MutableLiveData(false) - val isAlreadyRequest: LiveData = _isAlreadyRequest - - private val _event = MutableLiveData(null) - val event: LiveData = _event - - private val _uiEvent: NotNullMutableLiveData> = - NotNullMutableLiveData(UiEvent(RecruitmentPostDetailUiEvent.None)) - val uiEvent: NotNullLiveData> = _uiEvent - - private val myUid = requireNotNull(tokenRepository.getMyUid()) { - "[ERROR] 내 아이디를 가져오지 못했어요." - } - - init { - refresh() - } - - override fun refresh() { - viewModelScope.launch { - when (val result = recruitmentRepository.getEventRecruitment(eventId, recruitmentId)) { - is Failure -> _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.PostFetchFail) - NetworkError -> _recruitmentPost.value = _recruitmentPost.value.copy(isError = true) - is Success -> { - _recruitmentPost.value = RecruitmentPostUiState.create(result.data, myUid) - } - - is Unexpected -> - _uiEvent.value = - UiEvent(RecruitmentPostDetailUiEvent.UnexpectedError(result.error.toString())) - } - } - } - - fun deleteRecruitmentPost() { - viewModelScope.launch { - when (val result = recruitmentRepository.deleteRecruitment(eventId, recruitmentId)) { - is Failure -> _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.PostDeleteFail) - NetworkError -> _recruitmentPost.value = _recruitmentPost.value.copy(isError = true) - is Success -> - _uiEvent.value = - UiEvent(RecruitmentPostDetailUiEvent.PostDeleteComplete) - - is Unexpected -> - _uiEvent.value = - UiEvent(RecruitmentPostDetailUiEvent.UnexpectedError(result.error.toString())) - } - } - } - - fun reportRecruitment() { - viewModelScope.launch { - val result = recruitmentRepository.reportRecruitment( - recruitmentId, - recruitmentPost.value.memberId, - myUid, - ) - when (result) { - is Failure -> { - if (result.code == REPORT_DUPLICATE_ERROR_CODE) { - _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.ReportDuplicate) - } else { - _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.ReportFail) - } - } - - NetworkError -> - _recruitmentPost.value = _recruitmentPost.value.copy(isError = true) - - is Success -> _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.ReportComplete) - is Unexpected -> - _uiEvent.value = - UiEvent(RecruitmentPostDetailUiEvent.UnexpectedError(result.error.toString())) - } - } - } - - fun sendMessage(message: String) { - viewModelScope.launch { - when ( - val result = - messageRoomRepository.sendMessage( - myUid, - _recruitmentPost.value.memberId, - message, - ) - ) { - is Failure -> - _uiEvent.value = UiEvent(RecruitmentPostDetailUiEvent.MessageSendFail) - - NetworkError -> _recruitmentPost.value = _recruitmentPost.value.copy(isError = true) - is Success -> - _uiEvent.value = UiEvent( - RecruitmentPostDetailUiEvent.MessageSendComplete( - result.data, - _recruitmentPost.value.memberId, - ), - ) - - is Unexpected -> - _uiEvent.value = - UiEvent(RecruitmentPostDetailUiEvent.UnexpectedError(result.error.toString())) - } - } - } - - companion object { - const val EVENT_ID_KEY = "EVENT_ID_KEY" - const val RECRUITMENT_ID_KEY = "RECRUITMENT_ID_KEY" - - private const val REPORT_DUPLICATE_ERROR_CODE = 400 - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/SendMessageDialog.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/SendMessageDialog.kt index 5af1739ac..709dec637 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/SendMessageDialog.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/SendMessageDialog.kt @@ -8,22 +8,22 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels -import com.emmsale.databinding.DialogRecruitmentpostdetailSendMessageBinding +import com.emmsale.databinding.DialogRecruitmentdetailSendMessageBinding class SendMessageDialog : DialogFragment() { - private var _binding: DialogRecruitmentpostdetailSendMessageBinding? = null + private var _binding: DialogRecruitmentdetailSendMessageBinding? = null private val binding get() = _binding ?: throw IllegalStateException("쪽지 보내기 다이얼로그가 보이지 않을 때 바인딩 객체에 접근했습니다.") - private val viewModel: RecruitmentPostDetailViewModel by activityViewModels() + private val viewModel: RecruitmentDetailViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - _binding = DialogRecruitmentpostdetailSendMessageBinding.inflate(inflater, container, false) + _binding = DialogRecruitmentdetailSendMessageBinding.inflate(inflater, container, false) setupWindow() return binding.root } @@ -40,19 +40,14 @@ class SendMessageDialog : DialogFragment() { private fun setupDataBinding() { binding.lifecycleOwner = viewLifecycleOwner binding.vm = viewModel - binding.onSendButtonClick = ::onSendButtonClick - binding.onCancelButtonClick = ::onCancelButtonClick + binding.onSendButtonClick = { viewModel.sendMessage(it) } + binding.onCancelButtonClick = { dismiss() } } - private fun onSendButtonClick(message: String) { - viewModel.sendMessage(message) + fun clearText() { binding.etSendmessagedialogMessage.text.clear() } - private fun onCancelButtonClick() { - dismiss() - } - override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentPostDetailUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentPostDetailUiEvent.kt index dbc3fdf4a..8799aa49d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentPostDetailUiEvent.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentPostDetailUiEvent.kt @@ -1,8 +1,6 @@ package com.emmsale.presentation.ui.recruitmentDetail.uiState sealed interface RecruitmentPostDetailUiEvent { - object None : RecruitmentPostDetailUiEvent - data class UnexpectedError(val errorMessage: String) : RecruitmentPostDetailUiEvent object PostFetchFail : RecruitmentPostDetailUiEvent object PostDeleteComplete : RecruitmentPostDetailUiEvent object PostDeleteFail : RecruitmentPostDetailUiEvent diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentUiState.kt new file mode 100644 index 000000000..9915df276 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentDetail/uiState/RecruitmentUiState.kt @@ -0,0 +1,13 @@ +package com.emmsale.presentation.ui.recruitmentDetail.uiState + +import com.emmsale.data.model.Recruitment + +data class RecruitmentUiState( + val recruitment: Recruitment = Recruitment(), + val isMyPost: Boolean = false, +) { + constructor(recruitment: Recruitment, uid: Long) : this( + recruitment = recruitment, + isMyPost = uid == recruitment.writer.id, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentViewModel.kt deleted file mode 100644 index 710cb140f..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentList - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected -import com.emmsale.data.model.Recruitment -import com.emmsale.data.repository.interfaces.RecruitmentRepository -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostsUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class EventRecruitmentViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val recruitmentRepository: RecruitmentRepository, -) : ViewModel(), Refreshable { - val eventId: Long = requireNotNull(savedStateHandle[EVENT_ID_KEY]) { - "[ERROR] 행사 아이디를 가져오지 못했어요" - } - - private val _recruitments: NotNullMutableLiveData = - NotNullMutableLiveData( - RecruitmentPostsUiState(), - ) - val recruitments: NotNullLiveData = _recruitments - - override fun refresh() { - fetchRecruitments() - } - - private fun fetchRecruitments() { - changeRecruitmentsToLoadingState() - viewModelScope.launch { - when (val result = recruitmentRepository.getEventRecruitments(eventId)) { - is Failure, NetworkError -> changeRecruitmentsToErrorState() - is Success -> fetchSuccessRecruitments(result.data) - is Unexpected -> throw Throwable(result.error) - } - } - } - - private fun changeRecruitmentsToLoadingState() { - _recruitments.value = _recruitments.value.changeToLoadingState() - } - - private fun changeRecruitmentsToErrorState() { - _recruitments.value = _recruitments.value.changeToErrorState() - } - - private fun fetchSuccessRecruitments(recruitments: List) { - _recruitments.value = RecruitmentPostsUiState.from(recruitments) - } - - companion object { - const val EVENT_ID_KEY = "EVENT_ID_KEY" - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsFragment.kt similarity index 53% rename from android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentFragment.kt rename to android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsFragment.kt index a0f04dd23..1e1823298 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/EventRecruitmentFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsFragment.kt @@ -4,25 +4,25 @@ import android.content.Context import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.LinearLayoutManager import com.emmsale.R -import com.emmsale.databinding.FragmentEventRecruitmentBinding -import com.emmsale.presentation.base.BaseFragment +import com.emmsale.data.model.Recruitment +import com.emmsale.databinding.FragmentRecruitmentsBinding +import com.emmsale.presentation.base.NetworkFragment import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration import com.emmsale.presentation.ui.eventDetail.EventDetailActivity -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentPostDetailActivity -import com.emmsale.presentation.ui.recruitmentList.EventRecruitmentViewModel.Companion.EVENT_ID_KEY +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailActivity +import com.emmsale.presentation.ui.recruitmentList.RecruitmentsViewModel.Companion.EVENT_ID_KEY import com.emmsale.presentation.ui.recruitmentList.recyclerView.EventRecruitmentAdapter -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class EventRecruitmentFragment : - BaseFragment(), +class RecruitmentsFragment : + NetworkFragment(R.layout.fragment_recruitments), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("event_recruitment") { - override val layoutResId: Int = R.layout.fragment_event_recruitment - private val viewModel: EventRecruitmentViewModel by viewModels() + + override val viewModel: RecruitmentsViewModel by viewModels() private val recruitmentAdapter: EventRecruitmentAdapter by lazy { EventRecruitmentAdapter(::navigateToRecruitmentDetail) @@ -30,6 +30,14 @@ class EventRecruitmentFragment : private lateinit var eventDetailActivity: EventDetailActivity + private fun navigateToRecruitmentDetail(recruitment: Recruitment) { + RecruitmentDetailActivity.startActivity( + context = requireContext(), + eventId = viewModel.eventId, + recruitmentId = recruitment.id, + ) + } + override fun onAttach(context: Context) { super.onAttach(context) eventDetailActivity = context as EventDetailActivity @@ -38,10 +46,11 @@ class EventRecruitmentFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.lifecycleOwner = viewLifecycleOwner binding.vm = viewModel - initRecyclerView() - setUpRecruitments() + + setupRecruitmentsRecyclerView() + + observeRecruitments() } override fun onResume() { @@ -49,28 +58,19 @@ class EventRecruitmentFragment : viewModel.refresh() } - private fun navigateToRecruitmentDetail(recruitmentPostUiState: RecruitmentPostUiState) { - val intent = RecruitmentPostDetailActivity.getIntent( - requireContext(), - viewModel.eventId, - recruitmentPostUiState.id, - ) - startActivity(intent) - } - - private fun initRecyclerView() { + private fun setupRecruitmentsRecyclerView() { binding.rvRecruitment.adapter = recruitmentAdapter - binding.rvRecruitment.layoutManager = LinearLayoutManager(requireContext()) + binding.rvRecruitment.addItemDecoration(DividerItemDecoration(requireContext())) } - private fun setUpRecruitments() { - viewModel.recruitments.observe(viewLifecycleOwner) { recruitmentsUiState -> - recruitmentAdapter.submitList(recruitmentsUiState.list) + private fun observeRecruitments() { + viewModel.recruitments.observe(viewLifecycleOwner) { + recruitmentAdapter.submitList(it) } } companion object { - fun create(eventId: Long): EventRecruitmentFragment = EventRecruitmentFragment().apply { + fun create(eventId: Long): RecruitmentsFragment = RecruitmentsFragment().apply { arguments = Bundle().apply { putLong(EVENT_ID_KEY, eventId) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsViewModel.kt new file mode 100644 index 000000000..cae693d15 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/RecruitmentsViewModel.kt @@ -0,0 +1,42 @@ +package com.emmsale.presentation.ui.recruitmentList + +import androidx.lifecycle.SavedStateHandle +import com.emmsale.data.model.Recruitment +import com.emmsale.data.repository.interfaces.RecruitmentRepository +import com.emmsale.presentation.base.RefreshableViewModel +import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.common.livedata.NotNullMutableLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject + +@HiltViewModel +class RecruitmentsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val recruitmentRepository: RecruitmentRepository, +) : RefreshableViewModel() { + val eventId: Long = requireNotNull(savedStateHandle[EVENT_ID_KEY]) { + "[ERROR] 행사 아이디를 가져오지 못했어요" + } + + private val _recruitments = NotNullMutableLiveData(listOf()) + val recruitments: NotNullLiveData> = _recruitments + + init { + fetchRecruitments() + } + + private fun fetchRecruitments(): Job = fetchData( + fetchData = { recruitmentRepository.getEventRecruitments(eventId) }, + onSuccess = { _recruitments.value = it }, + ) + + override fun refresh(): Job = refreshData( + refresh = { recruitmentRepository.getEventRecruitments(eventId) }, + onSuccess = { _recruitments.value = it }, + ) + + companion object { + const val EVENT_ID_KEY = "EVENT_ID_KEY" + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/EventRecruitmentAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/EventRecruitmentAdapter.kt index 57e02ddd6..89a2f0ad7 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/EventRecruitmentAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/EventRecruitmentAdapter.kt @@ -3,11 +3,11 @@ package com.emmsale.presentation.ui.recruitmentList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostUiState +import com.emmsale.data.model.Recruitment class EventRecruitmentAdapter( - private val navigateToDetail: (RecruitmentPostUiState) -> Unit, -) : ListAdapter(diffUtil) { + private val navigateToDetail: (Recruitment) -> Unit, +) : ListAdapter(diffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecruitmentViewHolder = RecruitmentViewHolder.create(parent, navigateToDetail) @@ -16,15 +16,15 @@ class EventRecruitmentAdapter( } companion object { - val diffUtil = object : DiffUtil.ItemCallback() { + val diffUtil = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: RecruitmentPostUiState, - newItem: RecruitmentPostUiState, + oldItem: Recruitment, + newItem: Recruitment, ): Boolean = oldItem == newItem override fun areContentsTheSame( - oldItem: RecruitmentPostUiState, - newItem: RecruitmentPostUiState, + oldItem: Recruitment, + newItem: Recruitment, ): Boolean = oldItem.id == newItem.id } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/RecruitmentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/RecruitmentViewHolder.kt index c69957248..70af4389e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/RecruitmentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/recyclerView/RecruitmentViewHolder.kt @@ -3,26 +3,26 @@ package com.emmsale.presentation.ui.recruitmentList.recyclerView import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.emmsale.data.model.Recruitment import com.emmsale.databinding.ItemRecruitmentBinding -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostUiState class RecruitmentViewHolder( private val binding: ItemRecruitmentBinding, - private val navigateToDetail: (RecruitmentPostUiState) -> Unit, + private val navigateToDetail: (Recruitment) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { navigateToDetail(binding.recruitment!!) } } - fun bind(recruitment: RecruitmentPostUiState) { + fun bind(recruitment: Recruitment) { binding.recruitment = recruitment } companion object { fun create( parent: ViewGroup, - navigateToDetail: (RecruitmentPostUiState) -> Unit, + navigateToDetail: (Recruitment) -> Unit, ): RecruitmentViewHolder { val binding = ItemRecruitmentBinding.inflate( LayoutInflater.from(parent.context), diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/CompanionRequestTaskUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/CompanionRequestTaskUiState.kt deleted file mode 100644 index d6b9613cf..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/CompanionRequestTaskUiState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentList.uiState - -data class CompanionRequestTaskUiState( - val isLoading: Boolean = false, - val isError: Boolean = false, - val isSuccess: Boolean = false, -) { - fun changeToLoadingState() = copy( - isLoading = true, - isError = false, - isSuccess = false, - ) - - fun changeToErrorState() = copy( - isLoading = false, - isError = true, - isSuccess = false, - ) - - fun changeToSuccessState() = copy( - isLoading = false, - isError = false, - isSuccess = true, - ) -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostUiState.kt deleted file mode 100644 index 43d3e0d9d..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostUiState.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentList.uiState - -import com.emmsale.data.model.Recruitment - -data class RecruitmentPostUiState( - val id: Long = DEFAULT_RECRUITMENT_ID, - val memberId: Long = DEFAULT_MEMBER_ID, - val name: String = "", - val profileImageUrl: String = "", - val content: String = "", - val updatedAt: String = "", - val isMyPost: Boolean = false, - val isLoading: Boolean = false, - val isError: Boolean = false, -) { - fun changeToLoadingState() = copy( - isLoading = true, - ) - - fun changeToErrorState() = copy( - isLoading = false, - isError = true, - ) - - companion object { - private const val DEFAULT_RECRUITMENT_ID = -1L - private const val DEFAULT_MEMBER_ID = -1L - - val Loading: RecruitmentPostUiState = RecruitmentPostUiState( - id = DEFAULT_RECRUITMENT_ID, - memberId = DEFAULT_MEMBER_ID, - name = "", - profileImageUrl = "", - content = "", - updatedAt = "", - isMyPost = false, - isLoading = true, - isError = false, - ) - - fun from(recruitment: Recruitment): RecruitmentPostUiState = RecruitmentPostUiState( - id = recruitment.id, - memberId = recruitment.writer.id, - name = recruitment.writer.name, - profileImageUrl = recruitment.writer.profileImageUrl, - content = recruitment.content ?: "", - updatedAt = recruitment.updatedDate.toString(), - isMyPost = false, - isLoading = false, - isError = false, - ) - - fun create(recruitment: Recruitment, myUid: Long): RecruitmentPostUiState = - RecruitmentPostUiState( - id = recruitment.id, - memberId = recruitment.writer.id, - name = recruitment.writer.name, - profileImageUrl = recruitment.writer.profileImageUrl, - content = recruitment.content ?: "", - updatedAt = recruitment.updatedDate.toString(), - isMyPost = recruitment.writer.id == myUid, - isLoading = false, - isError = false, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostWritingUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostWritingUiState.kt deleted file mode 100644 index 343c41eb2..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostWritingUiState.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentList.uiState - -data class RecruitmentPostWritingUiState( - val isLoading: Boolean = false, - val isError: Boolean = false, - val isPostingSuccess: Boolean = false, - val isEditingSuccess: Boolean = false, - val writingMode: WritingModeUiState = WritingModeUiState.POST, -) { - fun changeToLoadingState(): RecruitmentPostWritingUiState = - copy( - isLoading = true, - isError = false, - isPostingSuccess = false, - ) - - fun changeToErrorState(): RecruitmentPostWritingUiState = - copy( - isLoading = false, - isError = true, - isPostingSuccess = false, - ) - - fun changeToPostSuccessState(): RecruitmentPostWritingUiState = - copy( - isLoading = false, - isError = false, - isPostingSuccess = true, - ) - - fun changeToEditSuccessState(): RecruitmentPostWritingUiState = - copy( - isLoading = false, - isError = false, - isEditingSuccess = true, - ) - - fun setWritingMode(mode: WritingModeUiState): RecruitmentPostWritingUiState = copy( - writingMode = mode, - ) -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostsUiState.kt deleted file mode 100644 index 5665b0d0e..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentList/uiState/RecruitmentPostsUiState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentList.uiState - -import com.emmsale.data.model.Recruitment -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class RecruitmentPostsUiState( - val list: List = listOf(), - override val fetchResult: FetchResult = FetchResult.LOADING, -) : FetchResultUiState() { - fun changeToLoadingState() = copy( - fetchResult = FetchResult.LOADING, - ) - - fun changeToErrorState() = copy( - fetchResult = FetchResult.ERROR, - ) - - companion object { - fun from(recruitments: List): RecruitmentPostsUiState { - return RecruitmentPostsUiState( - list = recruitments.map(RecruitmentPostUiState::from), - fetchResult = FetchResult.SUCCESS, - ) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingActivity.kt deleted file mode 100644 index 3ba96209d..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingActivity.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.emmsale.presentation.ui.recruitmentWriting - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import com.emmsale.R -import com.emmsale.databinding.ActivityRecruitmentPostWritingBinding -import com.emmsale.presentation.common.extension.showSnackBar -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentPostDetailActivity -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostWritingUiState -import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState -import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.EDIT -import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.POST -import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.EVENT_ID_KEY -import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.RECRUITMENT_CONTENT_KEY -import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.RECRUITMENT_ID_KEY -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class RecruitmentPostWritingActivity : AppCompatActivity() { - private val binding by lazy { ActivityRecruitmentPostWritingBinding.inflate(layoutInflater) } - private val viewModel: RecruitmentPostWritingViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initCompleteButtonClick() - initBackPressIconClick() - setUpRecruitmentWriting() - initBinding() - } - - private fun initBinding() { - setContentView(binding.root) - binding.vm = viewModel - binding.lifecycleOwner = this - } - - private fun setUpRecruitmentWriting() { - viewModel.recruitmentWriting.observe(this) { recruitmentWriting -> - onWritingResultStateChange(recruitmentWriting) - changeMenuText(recruitmentWriting.writingMode) - } - } - - private fun onWritingResultStateChange(recruitmentWriting: RecruitmentPostWritingUiState) { - when { - recruitmentWriting.isPostingSuccess -> { - showPostingSuccessMessage() - navigateToPostedPage() - setResult(RESULT_OK) - finish() - } - - recruitmentWriting.isEditingSuccess -> { - showEditingSuccessMessage() - setResult(RESULT_OK) - finish() - } - - recruitmentWriting.isError -> showPostingErrorMessage() - } - } - - private fun changeMenuText(writingMode: WritingModeUiState) { - binding.tbToolbar.menu.clear() - when (writingMode) { - POST -> binding.tbToolbar.inflateMenu(R.menu.menu_postwriting_toolbar) - - EDIT -> { - binding.tbToolbar.inflateMenu(R.menu.menu_postedit_toolbar) - binding.etRecruitmentwriting.setText(viewModel.recruitmentContentToEdit) - } - } - } - - private fun navigateToPostedPage() { - startActivity( - RecruitmentPostDetailActivity.getIntent( - this, - viewModel.eventId, - viewModel.postedRecruitmentId.value, - ), - ) - } - - private fun showPostingErrorMessage() = - binding.root.showSnackBar(getString(R.string.recruitmentpostwriting_register_error_message)) - - private fun showPostingSuccessMessage() = - binding.root.showSnackBar(getString(R.string.recruitmentpostwriting_register_success_message)) - - private fun showEditingSuccessMessage() = - binding.root.showSnackBar(getString(R.string.recruitmentpostwriting_edit_success_message)) - - private fun initBackPressIconClick() { - binding.tbToolbar.setNavigationOnClickListener { - finish() - } - } - - private fun initCompleteButtonClick() { - binding.tbToolbar.setOnMenuItemClickListener { - val content = binding.etRecruitmentwriting.text.toString() - if (content.isEmpty()) { - binding.root.showSnackBar(getString(R.string.recruitmentpostwriting_no_content_error_message)) - true - } - when (viewModel.recruitmentWriting.value.writingMode) { - EDIT -> viewModel.editRecruitment(content) - POST -> viewModel.postRecruitment(content) - } - true - } - } - - companion object { - fun getPostModeIntent(context: Context, eventId: Long): Intent { - val intent = Intent(context, RecruitmentPostWritingActivity::class.java) - intent.putExtra(EVENT_ID_KEY, eventId) - return intent - } - - fun getEditModeIntent( - context: Context, - eventId: Long, - recruitmentId: Long, - recruitmentContent: String, - ): Intent { - val intent = Intent(context, RecruitmentPostWritingActivity::class.java) - intent.putExtra(EVENT_ID_KEY, eventId) - intent.putExtra(RECRUITMENT_ID_KEY, recruitmentId) - intent.putExtra(RECRUITMENT_CONTENT_KEY, recruitmentContent) - return intent - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingViewModel.kt index 7eee15351..d4d384d73 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentPostWritingViewModel.kt @@ -1,110 +1,68 @@ package com.emmsale.presentation.ui.recruitmentWriting +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected import com.emmsale.data.repository.interfaces.RecruitmentRepository -import com.emmsale.presentation.common.firebase.analytics.logWriting -import com.emmsale.presentation.common.livedata.NotNullLiveData +import com.emmsale.presentation.base.NetworkViewModel import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.ui.recruitmentList.uiState.RecruitmentPostWritingUiState -import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.EDIT -import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.POST +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState +import com.emmsale.presentation.ui.recruitmentWriting.uiState.RecruitmentPostWritingUiEvent import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class RecruitmentPostWritingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val recruitmentRepository: RecruitmentRepository, -) : ViewModel() { +) : NetworkViewModel() { val eventId: Long = savedStateHandle[EVENT_ID_KEY] ?: DEFAULT_EVENT_ID private val recruitmentIdToEdit: Long? = savedStateHandle[RECRUITMENT_ID_KEY] - val recruitmentContentToEdit = savedStateHandle[RECRUITMENT_CONTENT_KEY] ?: DEFAULT_CONTENT + private val recruitmentContentToEdit: String = + savedStateHandle[RECRUITMENT_CONTENT_KEY] ?: DEFAULT_CONTENT - private val _recruitmentWriting: NotNullMutableLiveData = - NotNullMutableLiveData(RecruitmentPostWritingUiState()) - val recruitmentWriting: NotNullLiveData = - _recruitmentWriting + val writingMode: WritingModeUiState = + if (recruitmentIdToEdit == null) WritingModeUiState.POST else WritingModeUiState.EDIT - private val _postedRecruitmentId: NotNullMutableLiveData = - NotNullMutableLiveData(DEFAULT_POSTED_RECRUITMENT_ID) - val postedRecruitmentId: NotNullLiveData = _postedRecruitmentId + val content: MutableLiveData = MutableLiveData(recruitmentContentToEdit) + private val contentIsNotBlank: Boolean + get() = content.value?.isNotBlank() ?: false - init { - if (recruitmentIdToEdit != null) { - changeToEditMode() - } else { - changeToPostMode() - } + private val _canSubmit = NotNullMutableLiveData(true) + val canSubmit = MediatorLiveData(false).apply { + addSource(content) { value = canSubmit() } + addSource(_canSubmit) { value = canSubmit() } } - private fun changeToPostMode() { - _recruitmentWriting.value = _recruitmentWriting.value.setWritingMode( - POST, - ) - } + private fun canSubmit(): Boolean = contentIsNotBlank && isChanged() && _canSubmit.value - private fun changeToEditMode() { - _recruitmentWriting.value = _recruitmentWriting.value.setWritingMode( - EDIT, - ) - } + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent - fun postRecruitment(content: String) { - changeToLoadingState() - viewModelScope.launch { - when (val result = recruitmentRepository.postRecruitment(eventId, content)) { - is Failure, NetworkError -> changeToErrorState() - is Success -> { - _postedRecruitmentId.value = result.data - changeToPostSuccessState() - logWriting("recruitment", content, eventId) - } + fun postRecruitment(content: String): Job = command( + command = { recruitmentRepository.postRecruitment(eventId, content) }, + onSuccess = { _uiEvent.value = RecruitmentPostWritingUiEvent.PostComplete(it) }, + onFailure = { _, _ -> _uiEvent.value = RecruitmentPostWritingUiEvent.PostFail }, + onStart = { _canSubmit.value = false }, + onFinish = { _canSubmit.value = true }, + ) - is Unexpected -> throw Throwable(result.error) - } - } - } + fun editRecruitment(content: String): Job = command( + command = { + if (recruitmentIdToEdit == null) return@command Failure(-1, "") + recruitmentRepository.editRecruitment(eventId, recruitmentIdToEdit, content) + }, + onSuccess = { _uiEvent.value = RecruitmentPostWritingUiEvent.EditComplete }, + onFailure = { _, _ -> _uiEvent.value = RecruitmentPostWritingUiEvent.EditFail }, + onStart = { _canSubmit.value = false }, + onFinish = { _canSubmit.value = true }, + ) - fun editRecruitment(content: String) { - changeToLoadingState() - if (recruitmentIdToEdit == null) { - changeToErrorState() - return - } - viewModelScope.launch { - when ( - val result = - recruitmentRepository.editRecruitment(eventId, recruitmentIdToEdit, content) - ) { - is Failure, NetworkError -> changeToErrorState() - is Success -> changeToEditSuccessState() - is Unexpected -> throw Throwable(result.error) - } - } - } - - private fun changeToLoadingState() { - _recruitmentWriting.postValue(_recruitmentWriting.value.changeToLoadingState()) - } - - private fun changeToErrorState() { - _recruitmentWriting.postValue(_recruitmentWriting.value.changeToErrorState()) - } - - private fun changeToPostSuccessState() { - _recruitmentWriting.value = _recruitmentWriting.value.changeToPostSuccessState() - } - - private fun changeToEditSuccessState() { - _recruitmentWriting.postValue(_recruitmentWriting.value.changeToEditSuccessState()) - } + fun isChanged(): Boolean = content.value != recruitmentContentToEdit companion object { const val EVENT_ID_KEY = "EVENT_ID_KEY" @@ -114,7 +72,5 @@ class RecruitmentPostWritingViewModel @Inject constructor( const val RECRUITMENT_CONTENT_KEY = "RECRUITMENT_CONTENT_KEY" private const val DEFAULT_CONTENT = "" - - private const val DEFAULT_POSTED_RECRUITMENT_ID = -1L } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentWritingActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentWritingActivity.kt new file mode 100644 index 000000000..92cbd33d4 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/RecruitmentWritingActivity.kt @@ -0,0 +1,138 @@ +package com.emmsale.presentation.ui.recruitmentWriting + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels +import com.emmsale.R +import com.emmsale.databinding.ActivityRecruitmentWritingBinding +import com.emmsale.presentation.base.NetworkActivity +import com.emmsale.presentation.common.extension.showSnackBar +import com.emmsale.presentation.common.views.WarningDialog +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailActivity +import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.EDIT +import com.emmsale.presentation.ui.recruitmentList.uiState.WritingModeUiState.POST +import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.EVENT_ID_KEY +import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.RECRUITMENT_CONTENT_KEY +import com.emmsale.presentation.ui.recruitmentWriting.RecruitmentPostWritingViewModel.Companion.RECRUITMENT_ID_KEY +import com.emmsale.presentation.ui.recruitmentWriting.uiState.RecruitmentPostWritingUiEvent +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class RecruitmentWritingActivity : + NetworkActivity(R.layout.activity_recruitment_writing) { + + override val viewModel: RecruitmentPostWritingViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setupDataBinding() + setupBackPressedDispatcher() + setupToolbar() + + observeCanSubmit() + observeUiEvent() + } + + private fun setupDataBinding() { + setContentView(binding.root) + binding.vm = viewModel + } + + private fun setupBackPressedDispatcher() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (viewModel.isChanged()) showFinishConfirmDialog() else finish() + } + }, + ) + } + + private fun showFinishConfirmDialog() { + WarningDialog( + context = this, + title = getString(R.string.recruitmentpostwriting_writing_cancel_confirm_dialog_title), + message = getString(R.string.recruitmentpostwriting_writing_cancel_confirm_dialog_message), + positiveButtonLabel = getString(R.string.all_okay), + negativeButtonLabel = getString(R.string.all_cancel), + onPositiveButtonClick = { finish() }, + ).show() + } + + private fun setupToolbar() { + binding.tbToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + + when (viewModel.writingMode) { + POST -> binding.tbToolbar.inflateMenu(R.menu.menu_postwriting_toolbar) + EDIT -> binding.tbToolbar.inflateMenu(R.menu.menu_postedit_toolbar) + } + setupMenuClickListener() + } + + private fun setupMenuClickListener() { + binding.tbToolbar.setOnMenuItemClickListener { + val content = binding.etRecruitmentwriting.text.toString() + when (viewModel.writingMode) { + POST -> viewModel.postRecruitment(content) + EDIT -> viewModel.editRecruitment(content) + } + true + } + } + + private fun observeCanSubmit() { + viewModel.canSubmit.observe(this) { canSubmit -> + binding.tbToolbar.menu.findItem(R.id.register).isEnabled = canSubmit + } + } + + private fun observeUiEvent() { + viewModel.uiEvent.observe(this, ::handleUiEvent) + } + + private fun handleUiEvent(uiEvent: RecruitmentPostWritingUiEvent) { + when (uiEvent) { + RecruitmentPostWritingUiEvent.EditComplete -> { + setResult(RESULT_OK) + finish() + } + + RecruitmentPostWritingUiEvent.EditFail -> binding.root.showSnackBar(R.string.recruitmentpostwriting_edit_fail_message) + is RecruitmentPostWritingUiEvent.PostComplete -> { + RecruitmentDetailActivity.startActivity( + context = this, + eventId = viewModel.eventId, + recruitmentId = uiEvent.recruitmentId, + ) + finish() + } + + RecruitmentPostWritingUiEvent.PostFail -> binding.root.showSnackBar(R.string.recruitmentpostwriting_post_fail_message) + } + } + + companion object { + fun getPostModeIntent(context: Context, eventId: Long): Intent { + val intent = Intent(context, RecruitmentWritingActivity::class.java) + intent.putExtra(EVENT_ID_KEY, eventId) + return intent + } + + fun getEditModeIntent( + context: Context, + eventId: Long, + recruitmentId: Long, + recruitmentContent: String, + ): Intent { + val intent = Intent(context, RecruitmentWritingActivity::class.java) + intent.putExtra(EVENT_ID_KEY, eventId) + intent.putExtra(RECRUITMENT_ID_KEY, recruitmentId) + intent.putExtra(RECRUITMENT_CONTENT_KEY, recruitmentContent) + return intent + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/uiState/RecruitmentPostWritingUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/uiState/RecruitmentPostWritingUiEvent.kt new file mode 100644 index 000000000..1fd9b3ba7 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/recruitmentWriting/uiState/RecruitmentPostWritingUiEvent.kt @@ -0,0 +1,8 @@ +package com.emmsale.presentation.ui.recruitmentWriting.uiState + +sealed interface RecruitmentPostWritingUiEvent { + data class PostComplete(val recruitmentId: Long) : RecruitmentPostWritingUiEvent + object PostFail : RecruitmentPostWritingUiEvent + object EditComplete : RecruitmentPostWritingUiEvent + object EditFail : RecruitmentPostWritingUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventFragment.kt index e270cbe38..101bbd026 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventFragment.kt @@ -4,49 +4,56 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels import com.emmsale.R +import com.emmsale.data.model.Event import com.emmsale.databinding.FragmentScrappedEventBinding -import com.emmsale.presentation.base.BaseFragment +import com.emmsale.presentation.base.NetworkFragment import com.emmsale.presentation.common.ScrollTopListener import com.emmsale.presentation.ui.eventDetail.EventDetailActivity import com.emmsale.presentation.ui.scrappedEventList.recyclerView.ScrappedEventAdapter -import com.emmsale.presentation.ui.scrappedEventList.uiState.ScrappedEventUiState import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ScrappedEventFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_scrapped_event - private val viewModel: ScrappedEventViewModel by viewModels() +class ScrappedEventFragment : + NetworkFragment(R.layout.fragment_scrapped_event) { + + override val viewModel: ScrappedEventViewModel by viewModels() private val scrappedEventsAdapter: ScrappedEventAdapter by lazy { - ScrappedEventAdapter(::showEventDetail) + ScrappedEventAdapter(::navigateToEventDetail) + } + + private fun navigateToEventDetail(scrappedEvent: Event) { + EventDetailActivity.startActivity(requireContext(), scrappedEvent.id) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initBinding() - setUpScrappedEvents() - } - override fun onResume() { - super.onResume() - viewModel.refresh() + setupDataBinding() + setupScrappedEventsRecyclerView() + + observeScrappedEvents() } - private fun initBinding() { + private fun setupDataBinding() { binding.vm = viewModel + } + + private fun setupScrappedEventsRecyclerView() { binding.rvScrappedEvents.adapter = scrappedEventsAdapter binding.rvScrappedEvents.addOnScrollListener( ScrollTopListener(targetView = binding.fabScrollTop), ) } - private fun setUpScrappedEvents() { + private fun observeScrappedEvents() { viewModel.scrappedEvents.observe(viewLifecycleOwner) { scrappedEvents -> - scrappedEventsAdapter.submitList(scrappedEvents.list) + scrappedEventsAdapter.submitList(scrappedEvents) } } - private fun showEventDetail(scrappedEventUiState: ScrappedEventUiState) { - EventDetailActivity.startActivity(requireContext(), scrappedEventUiState.event.id) + override fun onResume() { + super.onResume() + viewModel.refresh() } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventViewModel.kt index 82ca545ff..7f141d19e 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/ScrappedEventViewModel.kt @@ -1,41 +1,32 @@ package com.emmsale.presentation.ui.scrappedEventList -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Success +import com.emmsale.data.model.Event import com.emmsale.data.repository.interfaces.EventRepository -import com.emmsale.presentation.common.FetchResult +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.scrappedEventList.uiState.ScrappedEventsUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class ScrappedEventViewModel @Inject constructor( private val eventRepository: EventRepository, -) : ViewModel(), Refreshable { - private val _scrappedEvents = NotNullMutableLiveData(ScrappedEventsUiState()) - val scrappedEvents: NotNullLiveData = _scrappedEvents +) : RefreshableViewModel() { + private val _scrappedEvents = NotNullMutableLiveData(listOf()) + val scrappedEvents: NotNullLiveData> = _scrappedEvents - override fun refresh() { - changeToLoadingState() - viewModelScope.launch { - when (val response = eventRepository.getScrappedEvents()) { - is Success -> _scrappedEvents.value = ScrappedEventsUiState.from(response.data) - else -> changeToErrorState() - } - } + init { + fetchScrappedEvents() } - private fun changeToLoadingState() { - _scrappedEvents.value = ScrappedEventsUiState(fetchResult = FetchResult.LOADING) - } + private fun fetchScrappedEvents(): Job = fetchData( + fetchData = { eventRepository.getScrappedEvents() }, + onSuccess = { _scrappedEvents.value = it }, + ) - private fun changeToErrorState() { - _scrappedEvents.value = - ScrappedEventsUiState(fetchResult = FetchResult.ERROR) - } + override fun refresh(): Job = refreshData( + refresh = { eventRepository.getScrappedEvents() }, + onSuccess = { _scrappedEvents.value = it }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventAdapter.kt index fa52bc703..a06d7b7df 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventAdapter.kt @@ -2,11 +2,11 @@ package com.emmsale.presentation.ui.scrappedEventList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.scrappedEventList.uiState.ScrappedEventUiState +import com.emmsale.data.model.Event class ScrappedEventAdapter( - private val onClickConference: (ScrappedEventUiState) -> Unit, -) : ListAdapter(ScrappedEventDiffUtil) { + private val onClickConference: (Event) -> Unit, +) : ListAdapter(ScrappedEventDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScrappedEventViewHolder = ScrappedEventViewHolder(parent, onClickConference) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventDiffUtil.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventDiffUtil.kt index 7fa0e2fef..874cc5bef 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventDiffUtil.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventDiffUtil.kt @@ -1,18 +1,18 @@ package com.emmsale.presentation.ui.scrappedEventList.recyclerView import androidx.recyclerview.widget.DiffUtil -import com.emmsale.presentation.ui.scrappedEventList.uiState.ScrappedEventUiState +import com.emmsale.data.model.Event -object ScrappedEventDiffUtil : DiffUtil.ItemCallback() { +object ScrappedEventDiffUtil : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: ScrappedEventUiState, - newItem: ScrappedEventUiState, + oldItem: Event, + newItem: Event, ): Boolean = - oldItem.event.id == newItem.event.id + oldItem.id == newItem.id override fun areContentsTheSame( - oldItem: ScrappedEventUiState, - newItem: ScrappedEventUiState, + oldItem: Event, + newItem: Event, ): Boolean = oldItem == newItem } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventViewHolder.kt index fa7106e7f..fd790480a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/recyclerView/ScrappedEventViewHolder.kt @@ -4,12 +4,12 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.emmsale.R +import com.emmsale.data.model.Event import com.emmsale.databinding.ItemScrappedEventBinding -import com.emmsale.presentation.ui.scrappedEventList.uiState.ScrappedEventUiState class ScrappedEventViewHolder( parent: ViewGroup, - onClickEvent: (ScrappedEventUiState) -> Unit, + onClickEvent: (Event) -> Unit, ) : ViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_scrapped_event, parent, false), ) { @@ -19,7 +19,7 @@ class ScrappedEventViewHolder( binding.onClickScrappedEvent = onClickEvent } - fun bind(event: ScrappedEventUiState) { + fun bind(event: Event) { binding.event = event } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventUiState.kt deleted file mode 100644 index 2c0f22ecd..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventUiState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.emmsale.presentation.ui.scrappedEventList.uiState - -import com.emmsale.data.model.Event - -data class ScrappedEventUiState( - val event: Event, -) { - companion object { - fun from(scrappedEvent: Event) = ScrappedEventUiState( - event = scrappedEvent, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventsUiState.kt deleted file mode 100644 index 82d4a4c8e..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/scrappedEventList/uiState/ScrappedEventsUiState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.emmsale.presentation.ui.scrappedEventList.uiState - -import com.emmsale.data.model.Event -import com.emmsale.presentation.common.FetchResult -import com.emmsale.presentation.common.FetchResultUiState - -data class ScrappedEventsUiState( - val list: List = listOf(), - override val fetchResult: FetchResult = FetchResult.LOADING, -) : FetchResultUiState() { - companion object { - fun from(scrappedEvents: List): ScrappedEventsUiState = - ScrappedEventsUiState( - list = scrappedEvents.map { ScrappedEventUiState.from(it) }, - fetchResult = FetchResult.SUCCESS, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt index 026fb727e..e6514d8d3 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt @@ -8,25 +8,25 @@ import android.view.View import androidx.fragment.app.viewModels import com.emmsale.R import com.emmsale.databinding.FragmentSettingBinding -import com.emmsale.presentation.base.BaseFragment +import com.emmsale.presentation.base.NetworkFragment import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegate import com.emmsale.presentation.common.firebase.analytics.FirebaseAnalyticsDelegateImpl import com.emmsale.presentation.common.views.WarningDialog -import com.emmsale.presentation.ui.blockMemberList.MemberBlockActivity +import com.emmsale.presentation.ui.blockMemberList.BlockedMembersActivity import com.emmsale.presentation.ui.login.LoginActivity import com.emmsale.presentation.ui.myCommentList.MyCommentsActivity import com.emmsale.presentation.ui.myRecruitmentList.MyRecruitmentActivity import com.emmsale.presentation.ui.notificationConfig.NotificationConfigActivity -import com.emmsale.presentation.ui.setting.uiState.MemberUiState +import com.emmsale.presentation.ui.setting.uiState.SettingUiEvent import com.emmsale.presentation.ui.useTerm.UseTermWebViewActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SettingFragment : - BaseFragment(), + NetworkFragment(R.layout.fragment_setting), FirebaseAnalyticsDelegate by FirebaseAnalyticsDelegateImpl("setting") { - override val layoutResId: Int = R.layout.fragment_setting - private val viewModel: SettingViewModel by viewModels() + + override val viewModel: SettingViewModel by viewModels() override fun onAttach(context: Context) { super.onAttach(context) @@ -35,42 +35,26 @@ class SettingFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initDataBinding() - setupUiLogic() - } - private fun initDataBinding() { - binding.viewModel = viewModel - binding.showWritings = ::showWritings - binding.showWrittenComments = ::showWrittenComments - binding.showNotificationSetting = ::navigateToNotificationConfig - binding.showBlocks = ::showBlocks - binding.showUseTerm = ::showUseTerm - binding.logout = ::logout - binding.showInquirePage = ::showInquirePage - } + setupDataBinding() - private fun showWritings() { - MyRecruitmentActivity.startActivity(requireContext()) + observeUiEvent() } - private fun showWrittenComments() { - MyCommentsActivity.startActivity(context ?: return) - } - - private fun navigateToNotificationConfig() { - NotificationConfigActivity.startActivity(requireContext()) - } - - private fun showBlocks() { - MemberBlockActivity.startActivity(context ?: return) - } - - private fun showUseTerm() { - UseTermWebViewActivity.startActivity(requireContext()) - } - - private fun logout() { + private fun setupDataBinding() { + binding.viewModel = viewModel + binding.onWritingsButtonClick = { MyRecruitmentActivity.startActivity(requireContext()) } + binding.onWrittenCommentsButtonClick = + { MyCommentsActivity.startActivity(requireContext()) } + binding.onNotificationSettingButtonClick = + { NotificationConfigActivity.startActivity(requireContext()) } + binding.onBlockMembersButtonClick = { BlockedMembersActivity.startActivity(requireContext()) } + binding.onUseTermButtonClick = { UseTermWebViewActivity.startActivity(requireContext()) } + binding.onLogoutButtonClick = ::showLogoutConfirmDialog + binding.onInquirePageButtonClick = ::navigateToInquirePage + } + + private fun showLogoutConfirmDialog() { WarningDialog( context = context ?: return, title = getString(R.string.logoutdialog_title), @@ -81,47 +65,21 @@ class SettingFragment : ).show() } - private fun showInquirePage() { + private fun navigateToInquirePage() { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(INQUIRE_PAGE_URL)) startActivity(intent) } - private fun setupUiLogic() { - setupLoginUiLogic() - setupMemberUiLogic() - } - - private fun setupLoginUiLogic() { - viewModel.isLogin.observe(viewLifecycleOwner) { - handleNotLogin(it) - } - } - - private fun handleNotLogin(isLogin: Boolean) { - if (!isLogin) { - LoginActivity.startActivity(requireContext()) - activity?.finish() - } - } - - private fun setupMemberUiLogic() { - viewModel.member.observe(viewLifecycleOwner) { - handleMemberDelete(it) - handleLogout(it) - } - } - - private fun handleMemberDelete(member: MemberUiState) { - if (member.isDeleted) { - LoginActivity.startActivity(context ?: return) - activity?.finish() - } + private fun observeUiEvent() { + viewModel.uiEvent.observe(viewLifecycleOwner, ::handleUiEvent) } - private fun handleLogout(member: MemberUiState) { - if (member.isLogout) { - LoginActivity.startActivity(context ?: return) - activity?.finish() + private fun handleUiEvent(uiEvent: SettingUiEvent) { + when (uiEvent) { + SettingUiEvent.Logout -> { + LoginActivity.startActivity(context ?: return) + activity?.finish() + } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingViewModel.kt index dab324342..a9534910d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingViewModel.kt @@ -1,19 +1,18 @@ package com.emmsale.presentation.ui.setting -import androidx.lifecycle.ViewModel +import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import com.emmsale.BuildConfig -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Member import com.emmsale.data.repository.interfaces.MemberRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.setting.uiState.MemberUiState +import com.emmsale.presentation.common.livedata.SingleLiveEvent +import com.emmsale.presentation.ui.setting.uiState.SettingUiEvent import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import javax.inject.Inject @@ -21,41 +20,34 @@ import javax.inject.Inject class SettingViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val memberRepository: MemberRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { - private val _isLogin = NotNullMutableLiveData(true) - val isLogin: NotNullLiveData = _isLogin + private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _member = NotNullMutableLiveData(MemberUiState.FIRST_LOADING) - val member: NotNullLiveData = _member + private val _member = NotNullMutableLiveData(Member()) + val member: NotNullLiveData = _member - private val _appVersion = NotNullMutableLiveData(BuildConfig.VERSION_NAME) - val appVersion: NotNullLiveData = _appVersion + val appVersion: String = BuildConfig.VERSION_NAME + + private val _uiEvent = SingleLiveEvent() + val uiEvent: LiveData = _uiEvent init { - refresh() + fetchMember() } - override fun refresh() { - viewModelScope.launch { - val token = tokenRepository.getToken() - if (token == null) { - _isLogin.value = false - return@launch - } - when (val result = memberRepository.getMember(token.uid)) { - is Failure, NetworkError -> _member.value = _member.value.changeToErrorState() - is Success -> _member.value = _member.value.changeMemberState(result.data) - is Unexpected -> throw Throwable(result.error) - } - } - } + private fun fetchMember(): Job = fetchData( + fetchData = { memberRepository.getMember(uid) }, + onSuccess = { _member.value = it }, + ) + + override fun refresh(): Job = refreshData( + refresh = { memberRepository.getMember(uid) }, + onSuccess = { _member.value = it }, + ) - fun logout() { - _member.value = _member.value.changeToLoadingState() - viewModelScope.launch { - tokenRepository.deleteToken() - _member.value = _member.value.changeToLogoutState() - } + fun logout(): Job = viewModelScope.launch { + tokenRepository.deleteToken() + _uiEvent.value = SettingUiEvent.Logout } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/MemberUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/MemberUiState.kt deleted file mode 100644 index 58addc9a1..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/MemberUiState.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.emmsale.presentation.ui.setting.uiState - -import com.emmsale.data.model.Member - -data class MemberUiState( - val isLoading: Boolean, - val isError: Boolean, - val isDeleted: Boolean, - val isLogout: Boolean, - val id: Long, - val imageUrl: String, - val name: String, - val githubId: String, -) { - - fun changeToLoadingState(): MemberUiState = copy( - isLoading = true, - ) - - fun changeToErrorState(): MemberUiState = copy( - isError = true, - ) - - fun changeToLogoutState(): MemberUiState = copy( - isLoading = false, - isError = false, - isLogout = true, - ) - - fun changeMemberState(member: Member): MemberUiState = copy( - isLoading = false, - isError = false, - id = member.id, - imageUrl = member.profileImageUrl, - name = member.name, - githubId = member.githubUrl, - ) - - companion object { - val FIRST_LOADING = MemberUiState( - isLoading = true, - isError = false, - isDeleted = false, - isLogout = false, - id = -1, - imageUrl = "", - name = "", - githubId = "", - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/SettingUiEvent.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/SettingUiEvent.kt new file mode 100644 index 000000000..8721809ce --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/uiState/SettingUiEvent.kt @@ -0,0 +1,5 @@ +package com.emmsale.presentation.ui.setting.uiState + +sealed interface SettingUiEvent { + object Logout : SettingUiEvent +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashActivity.kt index e7e5542aa..57faeffac 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashActivity.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashActivity.kt @@ -11,10 +11,12 @@ import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.window.SplashScreenView import androidx.activity.ComponentActivity -import androidx.activity.viewModels import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.lifecycleScope import com.emmsale.R +import com.emmsale.data.repository.interfaces.ConfigRepository +import com.emmsale.data.repository.interfaces.TokenRepository import com.emmsale.databinding.ActivitySplashBinding import com.emmsale.presentation.common.extension.addListener import com.emmsale.presentation.common.extension.isUpdateNeeded @@ -22,56 +24,67 @@ import com.emmsale.presentation.common.extension.showToast import com.emmsale.presentation.common.views.ConfirmDialog import com.emmsale.presentation.ui.login.LoginActivity import com.emmsale.presentation.ui.main.MainActivity -import com.emmsale.presentation.ui.splash.uiState.SplashUiState -import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManagerFactory import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint @SuppressLint("CustomSplashScreen") class SplashActivity : ComponentActivity() { - private val viewModel: SplashViewModel by viewModels() private val binding by lazy { ActivitySplashBinding.inflate(layoutInflater) } + @Inject + lateinit var tokenRepository: TokenRepository + + @Inject + lateinit var configRepository: ConfigRepository + + private val animationJob: Job = + lifecycleScope.launch(start = CoroutineStart.LAZY) { delay(SPLASH_ANIMATION_DURATION) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) setContentView(binding.root) - installSplashScreen() - setupSplashObserver() - } + animationJob.start() + checkAppUpdate() - private fun setupSplashObserver() { - viewModel.splash.observe(this) { splashState -> - when (splashState) { - is SplashUiState.Loading -> initSplashAnimation(splashState.splashTimeMs) - is SplashUiState.Done -> checkAppUpdate(splashState.isAutoLogin) - } - } + installSplashScreen() + setupSplashAnimation() } - private fun initSplashAnimation(durationMs: Long) { + private fun setupSplashAnimation() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return - splashScreen.setOnExitAnimationListener { splashScreenView -> - showSplashEndAnimation(splashScreenView, durationMs) - } + splashScreen.setOnExitAnimationListener(::showSplashEndAnimation) } - private fun showSplashEndAnimation(splashScreenView: SplashScreenView, durationMs: Long) { + private fun showSplashEndAnimation(splashScreenView: SplashScreenView) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return ObjectAnimator.ofFloat(splashScreenView, View.ALPHA, 1F, 0F).apply { interpolator = AccelerateDecelerateInterpolator() - duration = durationMs + duration = SPLASH_ANIMATION_DURATION doOnEnd { splashScreenView.remove() } }.start() } - private fun checkAppUpdate(isAutoLogin: Boolean) { + private fun checkAppUpdate() { val appUpdateManager = AppUpdateManagerFactory.create(this) appUpdateManager.appUpdateInfo.addListener( - onSuccess = { updateInfo -> updateInfo.handleUpdateInfo(isAutoLogin) }, + onSuccess = { info -> + animationJob.invokeOnCompletion { + when { + info.isUpdateNeeded() -> showUpdateConfirmDialog() + canAutoLogin() -> navigateToMain() + else -> navigateToLogin() + } + } + }, onFailed = { showToast(R.string.splash_not_installed_playstore) navigateToPlayStore() @@ -79,14 +92,7 @@ class SplashActivity : ComponentActivity() { ) } - private fun AppUpdateInfo.handleUpdateInfo(isAutoLogin: Boolean) { - when { - isUpdateNeeded() -> confirmUpdate() - else -> navigateToNextScreen(isAutoLogin) - } - } - - private fun confirmUpdate() { + private fun showUpdateConfirmDialog() { ConfirmDialog( this, title = getString(R.string.splash_app_update_title), @@ -107,24 +113,22 @@ class SplashActivity : ComponentActivity() { startActivity(intent) } - private fun navigateToNextScreen(isAutoLogin: Boolean) { - when { - isAutoLogin -> navigateToMainScreen() - else -> navigateToLoginScreen() - } - } + private fun canAutoLogin(): Boolean = + tokenRepository.getToken() != null && configRepository.getConfig().isAutoLogin - private fun navigateToMainScreen() { + private fun navigateToMain() { MainActivity.startActivity(this) finish() } - private fun navigateToLoginScreen() { + private fun navigateToLogin() { LoginActivity.startActivity(this) finish() } companion object { + private const val SPLASH_ANIMATION_DURATION = 1500L + fun getIntent(context: Context): Intent = Intent( context, SplashActivity::class.java, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashViewModel.kt deleted file mode 100644 index 0a372d630..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/SplashViewModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.emmsale.presentation.ui.splash - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.repository.interfaces.ConfigRepository -import com.emmsale.presentation.common.livedata.NotNullLiveData -import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.ui.splash.uiState.SplashUiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SplashViewModel @Inject constructor( - private val configRepository: ConfigRepository, -) : ViewModel() { - private val _splash = NotNullMutableLiveData(SplashUiState.Loading()) - val splash: NotNullLiveData = _splash - - init { - autoLogin() - } - - private fun autoLogin() { - viewModelScope.launch { - val autoLogin = configRepository.getConfig().isAutoLogin - val splashState = splash.value - - if (splashState is SplashUiState.Loading) { - delay(splashState.splashTimeMs) - } - - _splash.value = SplashUiState.Done(isAutoLogin = autoLogin) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/uiState/SplashUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/uiState/SplashUiState.kt deleted file mode 100644 index 0cba73535..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/splash/uiState/SplashUiState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.emmsale.presentation.ui.splash.uiState - -sealed class SplashUiState { - data class Done( - val isAutoLogin: Boolean = false, - ) : SplashUiState() - - data class Loading( - val splashTimeMs: Long = 1500L, - ) : SplashUiState() -} diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_all_vertical_divider.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_all_horizontal_divider.xml similarity index 100% rename from android/2023-emmsale/app/src/main/res/drawable/bg_all_vertical_divider.xml rename to android/2023-emmsale/app/src/main/res/drawable/bg_all_horizontal_divider.xml diff --git a/android/2023-emmsale/app/src/main/res/drawable/bg_primarynotification_header.xml b/android/2023-emmsale/app/src/main/res/drawable/bg_primarynotification_header.xml deleted file mode 100644 index 7a319cf2c..000000000 --- a/android/2023-emmsale/app/src/main/res/drawable/bg_primarynotification_header.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/drawable/ic_all_close.xml b/android/2023-emmsale/app/src/main/res/drawable/ic_all_close.xml index b19c5db3a..1047f4f63 100644 --- a/android/2023-emmsale/app/src/main/res/drawable/ic_all_close.xml +++ b/android/2023-emmsale/app/src/main/res/drawable/ic_all_close.xml @@ -9,4 +9,4 @@ android:strokeWidth="2" android:strokeColor="@color/back_button_color" android:strokeLineCap="round" /> - + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/drawable/ic_comment_child_comment_arrow.xml b/android/2023-emmsale/app/src/main/res/drawable/ic_comment_child_comment_arrow.xml index ab7b3ec16..26616955a 100644 --- a/android/2023-emmsale/app/src/main/res/drawable/ic_comment_child_comment_arrow.xml +++ b/android/2023-emmsale/app/src/main/res/drawable/ic_comment_child_comment_arrow.xml @@ -3,7 +3,7 @@ android:height="12dp" android:viewportWidth="12" android:viewportHeight="12"> - + diff --git a/android/2023-emmsale/app/src/main/res/drawable/ic_primarynotification_delete.xml b/android/2023-emmsale/app/src/main/res/drawable/ic_notification_delete.xml similarity index 100% rename from android/2023-emmsale/app/src/main/res/drawable/ic_primarynotification_delete.xml rename to android/2023-emmsale/app/src/main/res/drawable/ic_notification_delete.xml diff --git a/android/2023-emmsale/app/src/main/res/drawable/ic_writing_image_delete.xml b/android/2023-emmsale/app/src/main/res/drawable/ic_writing_image_delete.xml new file mode 100644 index 000000000..0104e180f --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/drawable/ic_writing_image_delete.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/drawable/img_profile_image_decoration.xml b/android/2023-emmsale/app/src/main/res/drawable/img_profile_image_decoration.xml index d6613073b..5a95f29a1 100644 --- a/android/2023-emmsale/app/src/main/res/drawable/img_profile_image_decoration.xml +++ b/android/2023-emmsale/app/src/main/res/drawable/img_profile_image_decoration.xml @@ -1,11 +1,15 @@ + android:viewportHeight="105"> + + + android:strokeColor="#7CF0B1"/> + diff --git a/android/2023-emmsale/app/src/main/res/layout-land/activity_event_detail.xml b/android/2023-emmsale/app/src/main/res/layout-land/activity_event_detail.xml index e254658b4..0a67036f3 100644 --- a/android/2023-emmsale/app/src/main/res/layout-land/activity_event_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout-land/activity_event_detail.xml @@ -1,6 +1,5 @@ @@ -13,7 +12,7 @@ - + @@ -52,7 +51,7 @@ android:id="@+id/tb_eventdetail" android:layout_width="0dp" android:layout_height="56dp" - app:title="@{vm.eventDetail.eventDetail.name}" + app:title="@{vm.event.name}" app:titleTextAppearance="@style/event_detail_toolbar_text_appearance" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -81,10 +80,10 @@ android:layout_marginStart="17dp" android:textSize="13sp" android:textStyle="bold" - app:eventApplyingStatus="@{vm.eventDetail.eventDetail.applicationStatus}" + app:eventApplyingStatus="@{vm.event.applicationStatus}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:visible="@{!(vm.eventDetail.eventDetail.progressStatus instanceof EventProgressStatus.Ended)}" + app:visible="@{!(vm.event.progressStatus instanceof EventProgressStatus.Ended)}" tools:text="종료" tools:textColor="@color/primary_color" /> @@ -97,7 +96,7 @@ app:layout_constraintBottom_toBottomOf="@+id/tv_application_status" app:layout_constraintStart_toEndOf="@+id/tv_application_status" app:layout_constraintTop_toTopOf="@+id/tv_application_status" - app:visible="@{!(vm.eventDetail.eventDetail.progressStatus instanceof EventProgressStatus.Ended)}" /> + app:visible="@{!(vm.event.progressStatus instanceof EventProgressStatus.Ended)}" /> @@ -166,11 +165,11 @@ android:layout_marginBottom="12dp" android:background="@drawable/bg_all_two_state_button" android:fontFamily="@font/nanum_square_bold" - android:onClick="@{_ -> navigateToUrl.invoke(vm.eventDetail.eventDetail.informationUrl)}" + android:onClick="@{_ -> navigateToUrl.invoke(vm.event.informationUrl)}" android:stateListAnimator="@null" android:text="@string/eventinformation_navigate_website" android:textColor="@color/white" - android:textSize="18sp" + android:textSize="16sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/tv_eventdetail_scrap" app:layout_constraintStart_toStartOf="parent" /> @@ -181,7 +180,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="20dp" android:clickable="true" - android:onClick="@{()->vm.handleEventScrap()}" + android:onClick="@{() -> vm.toggleIsScrapped()}" android:padding="6dp" android:src="@{vm.isScraped ? @drawable/ic_all_scrap_checked : @drawable/ic_all_scrap_unchecked}" app:layout_constraintBottom_toTopOf="@+id/tv_eventdetail_scrap" @@ -239,13 +238,13 @@ app:layout_constraintEnd_toEndOf="parent" android:onClick="@{()->navigateToWritingPost.invoke()}" android:src="@drawable/ic_recruitment_pencil" - android:visibility="@{vm.currentScreen == Screen.INFORMATION ? View.GONE : View.VISIBLE }" /> + app:visible="@{vm.currentScreen != Screen.INFORMATION}" /> - + app:onRefresh="@{() -> vm.refresh()}" + app:visible="@{vm.screenUiState == ScreenUiState.NETWORK_ERROR}" /> + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_member_block.xml b/android/2023-emmsale/app/src/main/res/layout/activity_blocked_members.xml similarity index 82% rename from android/2023-emmsale/app/src/main/res/layout/activity_member_block.xml rename to android/2023-emmsale/app/src/main/res/layout/activity_blocked_members.xml index 47b32c0a6..a80b157da 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_member_block.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_blocked_members.xml @@ -1,16 +1,17 @@ + + + type="com.emmsale.presentation.ui.blockMemberList.BlockedMembersViewModel" /> - + app:layout_constraintEnd_toEndOf="parent" + app:onRefresh="@{() -> viewModel.refresh()}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml b/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml index f0e416bc5..5bb313c0c 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_child_comments.xml @@ -13,7 +13,7 @@ + type="com.emmsale.presentation.ui.childCommentList.ChildCommentsViewModel" /> + tools:context=".presentation.ui.childCommentList.ChildCommentsActivity"> + + @@ -96,10 +97,10 @@ tools:ignore="LabelFor" app:layout_constraintEnd_toEndOf="@+id/tv_editmyprofile_name" /> - @@ -157,7 +158,7 @@ android:layout_marginTop="30dp" android:layout_marginEnd="22dp" android:contentDescription="@string/editmyprofile_member_image" - android:onClick="@{() -> editProfileImage.invoke()}" + android:onClick="@{() -> onProfileImageUpdateUiClick.invoke()}" app:imageUrl="@{viewModel.profile.member.profileImageUrl}" app:mask="@{@drawable/img_profile_image_masking}" app:layout_constraintEnd_toEndOf="parent" @@ -172,7 +173,7 @@ app:layout_constraintEnd_toEndOf="@id/iv_editmyprofile_member_image" app:layout_constraintBottom_toBottomOf="@id/iv_editmyprofile_member_image" android:elevation="3dp" - android:onClick="@{() -> editProfileImage.invoke()}" + android:onClick="@{() -> onProfileImageUpdateUiClick.invoke()}" android:contentDescription="@string/editmyprofile_profile_image_edit_button_image_description" /> - - + app:layout_constraintEnd_toEndOf="parent" + app:onRefresh="@{() -> viewModel.refresh()}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_event_detail.xml b/android/2023-emmsale/app/src/main/res/layout/activity_event_detail.xml index 67b14342b..20dddd612 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_event_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_event_detail.xml @@ -1,6 +1,5 @@ @@ -13,7 +12,7 @@ - + @@ -61,7 +60,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginHorizontal="17dp" - app:imageUrl="@{vm.eventDetail.eventDetail.posterImageUrl}" + app:imageUrl="@{vm.event.posterImageUrl}" app:layout_collapseMode="none" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="326:180" @@ -97,10 +96,10 @@ android:layout_marginTop="11dp" android:textSize="13sp" android:textStyle="bold" - app:eventApplyingStatus="@{vm.eventDetail.eventDetail.applicationStatus}" + app:eventApplyingStatus="@{vm.event.applicationStatus}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:visible="@{!(vm.eventDetail.eventDetail.progressStatus instanceof EventProgressStatus.Ended)}" + app:visible="@{!(vm.event.progressStatus instanceof EventProgressStatus.Ended)}" tools:text="D-21" tools:textColor="@color/primary_color" /> @@ -113,7 +112,7 @@ app:layout_constraintBottom_toBottomOf="@+id/tv_application_status" app:layout_constraintStart_toEndOf="@+id/tv_application_status" app:layout_constraintTop_toTopOf="@+id/tv_application_status" - app:visible="@{!(vm.eventDetail.eventDetail.progressStatus instanceof EventProgressStatus.Ended)}" /> + app:visible="@{!(vm.event.progressStatus instanceof EventProgressStatus.Ended)}" /> @@ -202,7 +201,7 @@ android:layout_gravity="bottom" android:background="@drawable/bg_shadow" android:clickable="true" - android:visibility="@{vm.currentScreen == Screen.INFORMATION ? View.VISIBLE : View.GONE }"> + app:visible="@{vm.currentScreen == Screen.INFORMATION}"> + app:visible="@{vm.currentScreen != Screen.INFORMATION}" /> - + android:layout_marginTop="?attr/actionBarSize" + app:onRefresh="@{() -> vm.refresh()}" + app:visible="@{vm.screenUiState == ScreenUiState.NETWORK_ERROR}" /> + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_event_search.xml b/android/2023-emmsale/app/src/main/res/layout/activity_event_search.xml index 0d14f9374..620bf4b64 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_event_search.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_event_search.xml @@ -126,12 +126,11 @@ app:visible="@{!etEventSearch.text.toString().isBlank()}" tools:visibility="gone" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml b/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml index 3abc3840f..9c0f67ffc 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_feed_detail.xml @@ -1,30 +1,31 @@ - + + + + name="onCommentSubmitButtonClick" + type="kotlin.jvm.functions.Function1<String, Unit>" /> + name="onUpdatedCommentSubmitButtonClick" + type="kotlin.jvm.functions.Function1<String, Unit>" /> - - - - - - - - - - - - - + + - - - - - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:text="@{vm.editingCommentContent}" + app:visible="@{vm.editingCommentId != null}" + app:isSubmitEnabled="@{vm.canSubmitComment}" + app:submitButtonLabel="@string/all_update_button_label" + app:cancelButtonLabel="@string/all_cancel" + app:onCancel="@{() -> onCommentUpdateCancelButtonClick.invoke()}" + app:onSubmit="@{(content) -> onUpdatedCommentSubmitButtonClick.invoke(content)}" /> - + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_feed_writing.xml b/android/2023-emmsale/app/src/main/res/layout/activity_feed_writing.xml index 8c3fb5d30..e12251ca2 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_feed_writing.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_feed_writing.xml @@ -5,7 +5,7 @@ - + + tools:context=".presentation.ui.recruitmentWriting.RecruitmentWritingActivity"> - - - - - - - - + - + - - - + + @@ -132,6 +123,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:visible="@{vm.feedUploadResult.peekContent().fetchResult == FetchResult.LOADING}" /> + app:visible="@{vm.screenUiState == ScreenUiState.LOADING}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_login.xml b/android/2023-emmsale/app/src/main/res/layout/activity_login.xml index fe572e426..b97767445 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_login.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_login.xml @@ -5,9 +5,15 @@ + + + + - + - @@ -46,10 +45,10 @@ android:layout_height="0dp" android:clipToPadding="false" android:paddingHorizontal="17dp" - android:paddingBottom="12dp" + android:paddingBottom="4dp" android:scrollbars="none" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toTopOf="@+id/cl_message_input" + app:layout_constraintBottom_toTopOf="@+id/btiw_send_message" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/divider" @@ -68,7 +67,7 @@ android:paddingStart="8dp" android:paddingEnd="20dp" android:visibility="gone" - app:layout_constraintBottom_toTopOf="@id/cl_message_input" + app:layout_constraintBottom_toTopOf="@id/btiw_send_message" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" tools:visibility="visible"> @@ -124,6 +123,18 @@ + + - - + app:visible="@{vm.messages.empty && vm.screenUiState == ScreenUiState.NONE}" /> - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_my_comments.xml b/android/2023-emmsale/app/src/main/res/layout/activity_my_comments.xml index 3e79dd4a6..66fe271ca 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_my_comments.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_my_comments.xml @@ -1,13 +1,14 @@ + + @@ -20,7 +21,7 @@ tools:context=".presentation.ui.setting.myComments.MyCommentsActivity"> @@ -49,25 +50,21 @@ style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{viewModel.comments.isLoading ? View.VISIBLE : View.GONE}" + app:visible="@{viewModel.screenUiState == ScreenUiState.LOADING}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/rv_mycomments_mycomments" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/tv_mycomments_toolbar" /> + app:layout_constraintTop_toBottomOf="@+id/tb_mycomments_toolbar" /> - + app:layout_constraintEnd_toEndOf="parent" + app:onRefresh="@{() -> viewModel.refresh()}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_notification_box.xml b/android/2023-emmsale/app/src/main/res/layout/activity_notification_box.xml deleted file mode 100644 index 64b25d0df..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/activity_notification_box.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_notification_config.xml b/android/2023-emmsale/app/src/main/res/layout/activity_notification_config.xml index 0b8aad2e2..535a91664 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_notification_config.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_notification_config.xml @@ -1,18 +1,39 @@ - + + + + + + + + + + + + + - @@ -166,6 +190,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="17dp" + android:onCheckedChanged="@{(v, isChecked) -> onCommentConfigSwitchClick.invoke(isChecked)}" android:thumb="@drawable/bg_notificationconfig_switch_thumb" android:track="@drawable/bg_notificationconfig_switch_track" app:layout_constraintBottom_toBottomOf="@+id/tv_notification_child_comment_title" @@ -173,30 +198,6 @@ app:layout_constraintTop_toTopOf="@+id/tv_notification_child_comment_title" tools:ignore="UseSwitchCompatOrMaterialXml" /> - - - - + app:layout_constraintStart_toStartOf="@+id/tv_notification_child_comment_title" + app:layout_constraintTop_toBottomOf="@+id/tv_notification_child_comment_title" /> + app:layout_constraintTop_toBottomOf="@id/tb_notification_config" /> - - + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_notification_tag_config.xml b/android/2023-emmsale/app/src/main/res/layout/activity_notification_tag_config.xml index f0c4070dc..13984b0d2 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_notification_tag_config.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_notification_tag_config.xml @@ -1,13 +1,14 @@ + + @@ -69,8 +70,9 @@ android:layout_marginVertical="17dp" android:background="@drawable/bg_all_two_state_button" android:gravity="center" - android:onClick="@{() -> viewModel.saveInterestEventTagIds()}" + android:onClick="@{() -> viewModel.saveInterestEventTag()}" android:paddingVertical="21dp" + android:enabled="@{viewModel.isChanged}" android:text="@string/notificationtagconfig_register_tag" android:textColor="@color/white" android:textSize="16sp" @@ -83,26 +85,22 @@ - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_notifications.xml b/android/2023-emmsale/app/src/main/res/layout/activity_notifications.xml new file mode 100644 index 000000000..ec5e66b02 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/activity_notifications.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml b/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml index c39a28145..348e46ade 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_onboarding.xml @@ -5,7 +5,8 @@ - + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_profile.xml b/android/2023-emmsale/app/src/main/res/layout/activity_profile.xml index 6f7e4e512..362f81cfe 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_profile.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_profile.xml @@ -1,13 +1,14 @@ + + @@ -106,12 +107,11 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@tools:sample/avatars" /> - @@ -220,24 +220,21 @@ style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{viewModel.profile.isLoading ? View.VISIBLE : View.GONE}" + app:visible="@{viewModel.screenUiState == ScreenUiState.LOADING}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintEnd_toEndOf="parent" + app:onRefresh="@{() -> viewModel.refresh()}" /> + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_detail.xml b/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_detail.xml similarity index 77% rename from android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_detail.xml rename to android/2023-emmsale/app/src/main/res/layout/activity_recruitment_detail.xml index 17b43de36..55d56bcc5 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_detail.xml @@ -7,19 +7,29 @@ - + + + + + + type="com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailViewModel" /> + + + + + tools:context=".presentation.ui.recruitmentDetail.RecruitmentDetailActivity"> @@ -61,7 +71,7 @@ android:layout_marginStart="9dp" android:layout_marginTop="18dp" android:fontFamily="@font/nanum_square_bold" - android:text="@{vm.recruitmentPost.name}" + android:text="@{vm.recruitment.recruitment.writer.name}" android:textColor="@color/black" android:textSize="12sp" app:layout_constraintStart_toEndOf="@+id/iv_recruitmentdetail_profile_image" @@ -75,7 +85,8 @@ android:layout_marginStart="9dp" android:layout_marginTop="6dp" android:fontFamily="@font/nanum_square_regular" - android:text="@{vm.recruitmentPost.updatedAt}" + app:dateText="@{vm.recruitment.recruitment.updatedDate}" + app:dateTimeFormatter="@{DateTimePattern.YEAR_DOT_MONTH_DOT_DAY}" android:textColor="@color/gray" android:textSize="11sp" app:layout_constraintStart_toEndOf="@+id/iv_recruitmentdetail_profile_image" @@ -90,7 +101,7 @@ android:layout_marginEnd="17dp" android:fontFamily="@font/nanum_square_regular" android:lineHeight="17.5dp" - android:text="@{vm.recruitmentPost.content}" + android:text="@{vm.recruitment.recruitment.content}" android:textColor="@color/black" android:textSize="13sp" app:layout_constraintEnd_toEndOf="parent" @@ -98,45 +109,26 @@ app:layout_constraintTop_toBottomOf="@+id/iv_recruitmentdetail_profile_image" tools:text="아무나 같이가실 분 구합니다! 역에서 만나서 같이가면 좋을 것 같아요! 많이들 신청해 주세용!!!" /> - + tools:srcCompat="@tools:sample/avatars" + android:contentDescription="@string/recruitmentpostdetail_writer_image_description" /> - - - @@ -157,11 +150,21 @@ android:id="@+id/progressBar2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{vm.recruitmentPost.loading? View.VISIBLE:View.GONE}" + app:visible="@{vm.screenUiState == ScreenUiState.LOADING}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_writing.xml b/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_writing.xml similarity index 92% rename from android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_writing.xml rename to android/2023-emmsale/app/src/main/res/layout/activity_recruitment_writing.xml index a5e528833..9eda14d0e 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_post_writing.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_recruitment_writing.xml @@ -7,6 +7,8 @@ + + @@ -15,7 +17,7 @@ + tools:context=".presentation.ui.recruitmentWriting.RecruitmentWritingActivity"> - diff --git a/android/2023-emmsale/app/src/main/res/layout/dialog_info.xml b/android/2023-emmsale/app/src/main/res/layout/dialog_info.xml index 5b7f7fd32..916e2aa7a 100644 --- a/android/2023-emmsale/app/src/main/res/layout/dialog_info.xml +++ b/android/2023-emmsale/app/src/main/res/layout/dialog_info.xml @@ -55,12 +55,11 @@ app:layout_constraintTop_toBottomOf="@+id/tv_infodialog_title" tools:text="댓글 삭제" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_accepted.xml b/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_accepted.xml deleted file mode 100644 index 47adbc173..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_accepted.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_reject_confirm.xml b/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_reject_confirm.xml deleted file mode 100644 index f66ad035d..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitment_reject_confirm.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentpostdetail_send_message.xml b/android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentdetail_send_message.xml similarity index 96% rename from android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentpostdetail_send_message.xml rename to android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentdetail_send_message.xml index f56955948..a7742e29b 100644 --- a/android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentpostdetail_send_message.xml +++ b/android/2023-emmsale/app/src/main/res/layout/dialog_recruitmentdetail_send_message.xml @@ -9,7 +9,7 @@ + type="com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailViewModel" /> + + @@ -57,9 +59,10 @@ - diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_event_information.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_event_information.xml index 4daafd66a..e547c3436 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_event_information.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_event_information.xml @@ -55,11 +55,11 @@ android:textColor="@color/black" android:textSize="12sp" app:dateTimeFormatter="@{DateTimePattern.MONTH_DAY_WEEKDAY}" - app:endDateTime="@{vm.eventDetail.eventDetail.applyingEndDate}" + app:endDateTime="@{vm.event.applyingEndDate}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/tv_event_info_date_title" app:layout_constraintTop_toTopOf="parent" - app:startDateTime="@{vm.eventDetail.eventDetail.applyingStartDate}" + app:startDateTime="@{vm.event.applyingStartDate}" tools:text="2023.08.15 12:00 ~ 2023.08.17 17:00" /> - + - + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_message_room.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_message_room.xml index aefdf40b9..19989bb47 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_message_room.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_message_room.xml @@ -1,12 +1,11 @@ - + @@ -32,18 +31,17 @@ app:titleTextAppearance="@style/ToolbarTitleFontStyle" /> - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + app:visible="@{vm.messageRooms.empty && vm.screenUiState == ScreenUiState.NONE}" /> + app:layout_constraintTop_toBottomOf="@id/tb_message_room_list" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_my_profile.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_my_profile.xml index ebc291bbe..5016b0cb8 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_my_profile.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_my_profile.xml @@ -1,13 +1,14 @@ + + @@ -56,7 +57,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="23dp" - android:text="@{viewModel.myProfile.member.name}" + android:text="@{viewModel.profile.name}" android:textSize="17sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="@+id/tv_myprofile_hello" @@ -69,7 +70,7 @@ android:layout_height="0dp" android:layout_marginVertical="9dp" android:layout_marginEnd="19dp" - android:text="@{viewModel.myProfile.member.description.isBlank() ? @string/profile_no_description : viewModel.myProfile.member.description }" + android:text="@{viewModel.profile.description.isBlank() ? @string/profile_no_description : viewModel.profile.description }" android:textSize="13sp" app:layout_constraintEnd_toStartOf="@+id/iv_myprofile_member_image" app:layout_constraintStart_toStartOf="@+id/tv_myprofile_membername" @@ -96,19 +97,18 @@ android:layout_marginEnd="22dp" android:contentDescription="@string/profile_memberimagedescription" android:importantForAccessibility="no" - app:imageUrl="@{viewModel.myProfile.member.profileImageUrl}" + app:imageUrl="@{viewModel.profile.profileImageUrl}" app:mask="@{@drawable/img_profile_image_masking}" app:canZoomIn="@{true}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:src="@tools:sample/avatars" /> - @@ -211,7 +211,6 @@ - - + app:layout_constraintEnd_toEndOf="parent" + app:visible="@{viewModel.screenUiState == ScreenUiState.NETWORK_ERROR}" + app:onRefresh="@{() -> viewModel.refresh()}" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml index acf1dbc9e..35cbaa385 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_club.xml @@ -8,82 +8,98 @@ + + - + app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/btn_next"> - + - + - + - + + + + + + + + + app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml index 17a3a73c1..3298eb718 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_education.xml @@ -8,83 +8,99 @@ + + - + app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/btn_next"> - + - + - + - + + + + + + + + + app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_field.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_field.xml index d127579bb..b15ef1dd3 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_field.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_field.xml @@ -8,83 +8,102 @@ + + - - - - - - - - - + app:navigationIcon="@drawable/ic_all_back" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/btn_next"> + + + + + + + + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml index 1bed677a0..d958cae27 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_onboarding_name.xml @@ -8,94 +8,117 @@ + + - + app:layout_constraintEnd_toEndOf="parent"> - + - + - + - + + + + + + + + + + + + - + app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_primary_notification.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_primary_notification.xml deleted file mode 100644 index 782736c71..000000000 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_primary_notification.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_event_recruitment.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_recruitments.xml similarity index 70% rename from android/2023-emmsale/app/src/main/res/layout/fragment_event_recruitment.xml rename to android/2023-emmsale/app/src/main/res/layout/fragment_recruitments.xml index 2a2717685..1bcbf4ea0 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_event_recruitment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_recruitments.xml @@ -1,31 +1,30 @@ - + + type="com.emmsale.presentation.ui.recruitmentList.RecruitmentsViewModel" /> + android:layout_marginBottom="93dp"> + app:visible="@{vm.screenUiState == ScreenUiState.LOADING}" /> - + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_scrapped_event.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_scrapped_event.xml index bf53d884a..80a85f2e9 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_scrapped_event.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_scrapped_event.xml @@ -1,7 +1,6 @@ @@ -10,6 +9,8 @@ + + @@ -20,94 +21,108 @@ android:layout_height="match_parent" tools:background="@color/white"> - + + + - + - + - + - + - + - + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml index f1ccc0859..7388bb1c3 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml @@ -1,47 +1,48 @@ + + @@ -52,27 +53,20 @@ - - + android:layout_height="wrap_content" + android:paddingTop="?attr/actionBarSize"> - @@ -166,7 +160,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:onClick="@{() -> showBlocks.invoke()}" + android:onClick="@{() -> onBlockMembersButtonClick.invoke()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/cl_setting_notification_setting_button"> @@ -200,7 +194,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:onClick="@{() -> showUseTerm.invoke()}" + android:onClick="@{() -> onUseTermButtonClick.invoke()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/cl_setting_blocks_button"> @@ -234,7 +228,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:onClick="@{() -> deleteMember.invoke()}" + android:onClick="@{() -> onWithdrawalButtonClick.invoke()}" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -259,7 +253,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:onClick="@{() -> showInquirePage.invoke()}" + android:onClick="@{() -> onInquirePageButtonClick.invoke()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/cl_setting_use_term_button"> @@ -327,7 +321,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:onClick="@{() -> logout.invoke()}" + android:onClick="@{() -> onLogoutButtonClick.invoke()}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/cl_setting_version_info_button"> @@ -352,24 +346,21 @@ style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{viewModel.member.isLoading ? View.VISIBLE : View.GONE}" + app:visible="@{viewModel.screenUiState == ScreenUiState.LOADING}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml index 50ac25259..9fd455eba 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_all_comment.xml @@ -23,7 +23,7 @@ + type="kotlin.jvm.functions.Function2<Boolean, Comment, Unit>" /> + tools:showIn="@layout/activity_blocked_members"> @@ -10,7 +10,7 @@ + type="com.emmsale.data.model.BlockedMember" /> - - - + @@ -21,7 +19,7 @@ + type="com.emmsale.data.model.notification.ChildCommentNotification" /> + type="Function1<Notification, Unit>" /> @@ -47,9 +45,9 @@ android:id="@+id/iv_member_profile" android:layout_width="48dp" android:layout_height="48dp" - android:contentDescription="@string/primarynotification_profile_image_desc" + android:contentDescription="@string/notifications_profile_image_desc" android:scaleType="centerCrop" - app:imageUrl="@{commentNotification.commenterProfileImageUrl}" + app:imageUrl="@{commentNotification.comment.writer.profileImageUrl}" app:isCircle="@{true}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -61,9 +59,10 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="15dp" + android:layout_marginEnd="9dp" android:ellipsize="end" android:maxLines="1" - android:text="@{commentNotification.commentContent}" + android:text="@{commentNotification.comment.content}" android:textColor="@color/black" android:textSize="14sp" android:textStyle="bold" @@ -87,15 +86,15 @@ diff --git a/android/2023-emmsale/app/src/main/res/layout/item_feed.xml b/android/2023-emmsale/app/src/main/res/layout/item_feed.xml index ee94adbc2..bfad0bc75 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_feed.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_feed.xml @@ -143,15 +143,5 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/img_all_loading" /> - - - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_feeddetail_feed_detail.xml b/android/2023-emmsale/app/src/main/res/layout/item_feeddetail_feed_detail.xml index b1e74878b..5b07fb3ed 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_feeddetail_feed_detail.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_feeddetail_feed_detail.xml @@ -12,11 +12,11 @@ + name="feed" + type="com.emmsale.data.model.Feed" /> @@ -31,8 +31,8 @@ android:layout_marginStart="17dp" android:layout_marginTop="14dp" android:contentDescription="@string/feeddetail_author_image_description" - android:onClick="@{() -> onProfileImageClick.invoke(uiState.feedDetail.writer.id)}" - app:imageUrl="@{uiState.feedDetail.writer.profileImageUrl}" + android:onClick="@{() -> onAuthorImageClick.invoke(feed.writer.id)}" + app:imageUrl="@{feed.writer.profileImageUrl}" app:isCircle="@{true}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -44,7 +44,7 @@ android:layout_height="wrap_content" android:layout_marginStart="9dp" android:layout_marginTop="4dp" - android:text="@{uiState.feedDetail.writer.name}" + android:text="@{feed.writer.name}" android:textSize="13sp" android:textStyle="bold" app:layout_constraintStart_toEndOf="@+id/iv_feeddetail_feed_author_image" @@ -58,7 +58,7 @@ android:layout_marginBottom="3dp" android:textColor="@color/gray" android:textSize="11sp" - app:dateText="@{uiState.feedDetail.updatedAt}" + app:dateText="@{feed.updatedAt}" app:dateTimeFormatter="@{DateTimePattern.RELATIVE_TIME}" app:layout_constraintBottom_toBottomOf="@+id/iv_feeddetail_feed_author_image" app:layout_constraintStart_toStartOf="@+id/tv_feeddetail_feed_author_name" @@ -72,7 +72,7 @@ android:text="@string/all_is_updated" android:textColor="@color/gray" android:textSize="11sp" - android:visibility="@{uiState.isUpdated ? View.VISIBLE : View.GONE}" + app:visible="@{feed.isUpdated}" app:layout_constraintBottom_toBottomOf="@+id/tv_feeddetail_last_modified_date" app:layout_constraintStart_toEndOf="@+id/tv_feeddetail_last_modified_date" app:layout_constraintTop_toTopOf="@+id/tv_feeddetail_last_modified_date" /> @@ -83,7 +83,7 @@ android:layout_height="wrap_content" android:layout_marginTop="14dp" android:layout_marginEnd="17dp" - android:text="@{uiState.feedDetail.title}" + android:text="@{feed.title}" android:textSize="15sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" @@ -99,7 +99,7 @@ android:autoLink="web" android:lineSpacingExtra="2dp" android:linksClickable="true" - android:text="@{uiState.feedDetail.content}" + android:text="@{feed.content}" app:layout_constraintEnd_toEndOf="@+id/tv_feeddetail_title" app:layout_constraintStart_toStartOf="@+id/tv_feeddetail_title" app:layout_constraintTop_toBottomOf="@+id/tv_feeddetail_title" @@ -122,7 +122,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="5dp" - android:text="@{uiState.commentsCount}" + android:text="@{feed.commentCount}" android:textColor="@color/primary_color" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="@+id/iv_feeddetail_comment_icon" diff --git a/android/2023-emmsale/app/src/main/res/layout/item_primary_notification.xml b/android/2023-emmsale/app/src/main/res/layout/item_interest_event_notification.xml similarity index 77% rename from android/2023-emmsale/app/src/main/res/layout/item_primary_notification.xml rename to android/2023-emmsale/app/src/main/res/layout/item_interest_event_notification.xml index 2987180a7..4cc7d9284 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_primary_notification.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_interest_event_notification.xml @@ -11,9 +11,7 @@ - - - + @@ -21,7 +19,7 @@ + type="com.emmsale.data.model.notification.InterestEventNotification" /> + type="Function1<Notification, Unit>" /> + android:paddingVertical="18dp"> @@ -62,7 +59,7 @@ android:ellipsize="end" android:fontFamily="@font/nanum_square_extra_bold" android:maxLines="1" - android:text="@string/primarynotification_interest_event_message" + android:text="@string/notifications_interest_event_message" android:textColor="@color/black" android:textSize="14sp" app:layout_constraintEnd_toStartOf="@id/btn_delete" @@ -71,15 +68,16 @@ @@ -101,14 +99,13 @@ android:id="@+id/btn_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:clickable="true" - android:contentDescription="@string/primarynotification_past_notification_delete" - android:focusable="true" - android:onClick="@{() -> onDeleteClick.invoke(interestEventNotification.notificationId)}" - android:padding="4dp" - android:src="@drawable/ic_primarynotification_delete" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/notifications_past_notification_delete" + android:onClick="@{() -> onDeleteClick.invoke(interestEventNotification.id)}" + android:padding="6dp" + android:layout_marginEnd="-6dp" + android:src="@drawable/ic_notification_delete" app:layout_constraintBottom_toBottomOf="@+id/tv_comment" - app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tv_comment" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml index 271f6c721..9fa60b575 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml @@ -9,19 +9,21 @@ + + + type="com.emmsale.data.model.Comment" /> + name="onCommentClick" + type="kotlin.jvm.functions.Function2<Long, Long, Unit>" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_past_notification_header.xml b/android/2023-emmsale/app/src/main/res/layout/item_past_notification_header.xml index 5aea253cc..e5c96b41c 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_past_notification_header.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_past_notification_header.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginTop="30dp" - android:text="@string/primarynotification_past_notification_header_title" + android:text="@string/notifications_past_notification_header_title" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -32,7 +32,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:onClick="@{() -> onDeleteAllClick.invoke()}" android:paddingVertical="10dp" - android:text="@string/primarynotification_past_notification_delete_all" + android:text="@string/notifications_past_notification_delete_all" android:textColor="@color/light_gray" android:textSize="12sp" android:textStyle="bold" @@ -40,12 +40,10 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tv_past_notification_title" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_recent_notification_header.xml b/android/2023-emmsale/app/src/main/res/layout/item_recent_notification_header.xml index da5e85790..642c11beb 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_recent_notification_header.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_recent_notification_header.xml @@ -8,23 +8,30 @@ + android:layout_height="wrap_content"> + + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/layout/item_recruitment.xml b/android/2023-emmsale/app/src/main/res/layout/item_recruitment.xml index 18a210e45..0b6e11642 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_recruitment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_recruitment.xml @@ -5,9 +5,11 @@ + + + type="com.emmsale.data.model.Recruitment" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/item_scrapped_event.xml b/android/2023-emmsale/app/src/main/res/layout/item_scrapped_event.xml index 834498c6d..300cc9069 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_scrapped_event.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_scrapped_event.xml @@ -5,10 +5,10 @@ - - + + @@ -17,11 +17,11 @@ + type="com.emmsale.data.model.Event" /> + type="kotlin.jvm.functions.Function1<Event, Unit>" /> @@ -65,7 +65,7 @@ android:layout_width="1dp" android:layout_height="0dp" android:layout_marginStart="6dp" - app:visible="@{!(event.event.progressStatus instanceof EventProgressStatus.Ended)}" + app:visible="@{!(event.progressStatus instanceof EventProgressStatus.Ended)}" android:background="@color/light_gray" app:layout_constraintBottom_toBottomOf="@+id/tv_application_status" app:layout_constraintStart_toEndOf="@+id/tv_application_status" @@ -80,7 +80,7 @@ android:textSize="13sp" android:textStyle="bold" app:layout_goneMarginStart="0dp" - app:eventProgressStatus="@{event.event.progressStatus}" + app:eventProgressStatus="@{event.progressStatus}" app:layout_constraintStart_toEndOf="@+id/divider_event_status" app:layout_constraintTop_toBottomOf="@+id/iv_event_poster" tools:text="D-21" @@ -90,7 +90,7 @@ android:id="@+id/tv_event_name" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@{event.event.name}" + android:text="@{event.name}" android:textColor="@color/black" android:layout_marginTop="9dp" android:textSize="15sp" @@ -108,7 +108,7 @@ android:textSize="12sp" app:layout_constraintStart_toStartOf="@+id/iv_event_poster" app:layout_constraintTop_toBottomOf="@+id/tv_event_name" - app:paymentType="@{event.event.paymentType}" + app:paymentType="@{event.paymentType}" tools:text="무료" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/item_writing_image.xml b/android/2023-emmsale/app/src/main/res/layout/item_writing_image.xml index 2612a0b30..bcec69521 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_writing_image.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_writing_image.xml @@ -14,7 +14,7 @@ type="String" /> @@ -26,22 +26,20 @@ android:id="@+id/iv_item_post_image" android:layout_width="140dp" android:layout_height="140dp" - android:layout_marginEnd="10dp" app:imageUrl="@{imageUrl}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:roundedImageRadius="@{10}" + app:canZoomIn="@{true}" tools:src="@drawable/img_all_loading" /> diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_status.xml b/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_status.xml index 4c5af6a84..214870543 100644 --- a/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_status.xml +++ b/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_status.xml @@ -28,11 +28,10 @@ android:layout_marginTop="14dp" app:layout_constraintTop_toBottomOf="@+id/tv_filter_category" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_tag.xml b/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_tag.xml index 427c06a0e..b233bf8a0 100644 --- a/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_tag.xml +++ b/android/2023-emmsale/app/src/main/res/layout/layout_filter_competition_tag.xml @@ -36,11 +36,10 @@ - diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_status.xml b/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_status.xml index 4c60bcf3a..f1ec3b774 100644 --- a/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_status.xml +++ b/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_status.xml @@ -28,11 +28,10 @@ android:layout_marginTop="14dp" app:layout_constraintTop_toBottomOf="@+id/tv_filter_category" /> - diff --git a/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_tag.xml b/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_tag.xml index 58b4a5e60..84eb3d930 100644 --- a/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_tag.xml +++ b/android/2023-emmsale/app/src/main/res/layout/layout_filter_conference_tag.xml @@ -36,11 +36,10 @@ - diff --git a/android/2023-emmsale/app/src/main/res/values-night/themes.xml b/android/2023-emmsale/app/src/main/res/values-night/themes.xml index d2132ac6b..73c8fe8cb 100644 --- a/android/2023-emmsale/app/src/main/res/values-night/themes.xml +++ b/android/2023-emmsale/app/src/main/res/values-night/themes.xml @@ -11,6 +11,12 @@ @style/ThemeOverlay.MaterialComponents.BottomSheetDialog + + @style/PrimaryToolbarStyle + + @style/PrimaryActionMenuTextAppearance + @color/all_enable_primary + @style/IconTextButtonStyle @style/FilterTagStyle diff --git a/android/2023-emmsale/app/src/main/res/values/date_time_patterns.xml b/android/2023-emmsale/app/src/main/res/values/date_time_patterns.xml index 086f56d54..0a0bc04ca 100644 --- a/android/2023-emmsale/app/src/main/res/values/date_time_patterns.xml +++ b/android/2023-emmsale/app/src/main/res/values/date_time_patterns.xml @@ -3,12 +3,13 @@ M월 d일(E) a h:mm yyyy.MM.dd + yyyy.MM.dd MM.dd yyyy년 MM월 dd일 E요일 + yyyy-MM-dd %s ~ %s MM.dd - yyyy.MM.dd %d분 전 %d시간 전 diff --git a/android/2023-emmsale/app/src/main/res/values/ids.xml b/android/2023-emmsale/app/src/main/res/values/ids.xml index 02740cc2a..0248b09d1 100644 --- a/android/2023-emmsale/app/src/main/res/values/ids.xml +++ b/android/2023-emmsale/app/src/main/res/values/ids.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/android/2023-emmsale/app/src/main/res/values/strings.xml b/android/2023-emmsale/app/src/main/res/values/strings.xml index 5ac1c8cbb..2b503f089 100644 --- a/android/2023-emmsale/app/src/main/res/values/strings.xml +++ b/android/2023-emmsale/app/src/main/res/values/strings.xml @@ -48,12 +48,15 @@ 오프라인 온/오프라인 종료 + 오류 + 데이터를 불러올 수 없습니다. + 알 수 없는 오류가 발생했습니다. 업데이트 새로운 버전이 출시되었어요!\n지금 바로 커디 스토어로 이동하시겠어요? 업데이트 없이 서비스를 이용하실 수 없어요 😥 - 회원정보 업데이트를 실패했어요! + 회원가입에 실패했어요! 온보딩 화면 뒤로가기 1/4 @@ -80,10 +83,10 @@ 동아리 활동이 있나요? 동아리 활동을 했다면 추가해 주세요. - 댓글에 답글이 달렸어요. - %s: %s - 대댓글 작성 알림 채널 - 대댓글 작성 알림을 받기 위한 알림 채널입니다. + 댓글이 달렸어요. + %s: %s + 댓글 작성 알림 채널 + 댓글 작성 알림을 받기 위한 알림 채널입니다. 관심 태그 행사가 업데이트 되었어요. 관심 태그 행사 알림 채널 관심 태그 행사 알림을 받기 위한 알림 채널입니다. @@ -245,8 +248,8 @@ 댓글 같이가요 상세정보 보기 탭 - 스크랩 불가 - 스크랩 삭제 불가 + 스크랩 불가 + 스크랩 해제 불가 비용 장소 @@ -254,19 +257,20 @@ 글을 불러올 수 없습니다. 관리자에게 문의하세요. 모집글은 한 번만 작성이 가능합니다!! + 모집글을 작성할 수 있는지 확인하는 데 실패했어요. 가장 먼저 행사에 같이 갈 사람을 구해보세요! 아직 같이가요 게시글이 없어요! 가장 먼저 게시글을 작성해보세요! 아직 게시물이 없어요! - 내용이 없으면 모집글을 올릴 수 없어요 - 글을 등록하였습니다 - 등록에 실패하였습니다! 인터넷을 확인하시고, 관리자에게 문의해주세요 글쓰기 + 모집글 작성에 실패했어요 😥 + 모집글 수정에 실패했어요 😥 + 작성 취소 + 작성하던 글이 사라집니다.\n나가시겠습니까? 등록 모집글을 작성해주세요. - 수정에 성공하였습니다 수정 요청 메세지를 작성해주세요. @@ -278,10 +282,10 @@ 함께해요 함께하기 요청 + 함께해요 글 작성자의 프로필 이미지 요청 완료 요청에 성공했어요 요청에 실패했어요 - 글을 삭제했어요! 함께해요 글을 삭제하는 데 실패했어요 😥 수정 삭제 @@ -319,16 +323,16 @@ 로그아웃 취소 - 새로운 알림 - 지난 알림 - 전체 삭제 - 유저 프로필 - 지난 알림 삭제 - 댓글에 답글이 달렸어요. - 관심 태그 행사가 업데이트 되었어요. - 알림 삭제를 할 수 없어요! - 지난 알림을 모두 삭제하시겠어요? - 알림 삭제 + 새로운 알림 + 지난 알림 + 전체 삭제 + 유저 프로필 + 지난 알림 삭제 + 댓글에 답글이 달렸어요. + 관심 태그 행사가 업데이트 되었어요. + 알림 삭제를 할 수 없어요! + 지난 알림을 모두 삭제하시겠어요? + 알림 삭제 프로필 수정 diff --git a/android/2023-emmsale/app/src/main/res/values/themes.xml b/android/2023-emmsale/app/src/main/res/values/themes.xml index 84fe633ac..43dfbc06a 100644 --- a/android/2023-emmsale/app/src/main/res/values/themes.xml +++ b/android/2023-emmsale/app/src/main/res/values/themes.xml @@ -13,10 +13,11 @@ @style/ThemeOverlay.MaterialComponents.BottomSheetDialog + @style/PrimaryToolbarStyle @style/PrimaryActionMenuTextAppearance - @color/primary_color + @color/all_enable_primary @style/IconTextButtonStyle @style/FilterTagStyle