From c2618669c88e090c7154021b4af0c4bc215a5dc0 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 1 Aug 2023 07:04:16 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 8 + app/src/main/AndroidManifest.xml | 1 + .../kr/co/vividnext/sodalive/agora/Agora.kt | 205 +++ .../co/vividnext/sodalive/common/Constants.kt | 2 + .../sodalive/common/SodaLiveService.kt | 90 ++ .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 22 +- .../explorer/profile/MemberBlockRequest.kt | 5 + .../kr/co/vividnext/sodalive/live/LiveApi.kt | 89 ++ .../vividnext/sodalive/live/LiveFragment.kt | 111 +- .../vividnext/sodalive/live/LiveRepository.kt | 116 +- .../vividnext/sodalive/live/LiveViewModel.kt | 37 +- .../live/room/EnterOrQuitLiveRoomRequest.kt | 2 +- .../sodalive/live/room/LiveRoomActivity.kt | 1404 +++++++++++++++++ .../sodalive/live/room/LiveRoomDialog.kt | 99 ++ .../sodalive/live/room/LiveRoomRequestType.kt | 11 + .../sodalive/live/room/LiveRoomViewModel.kt | 778 +++++++++ .../SetManagerOrSpeakerOrAudienceRequest.kt | 8 + .../sodalive/live/room/chat/LiveRoomChat.kt | 336 ++++ .../live/room/chat/LiveRoomChatAdapter.kt | 99 ++ .../live/room/chat/LiveRoomChatRawMessage.kt | 17 + .../donation/DeleteLiveRoomDonationMessage.kt | 8 + .../GetLiveRoomDonationStatusResponse.kt | 16 + .../GetLiveRoomDonationTotalResponse.kt | 7 + .../room/donation/LiveRoomDonationDialog.kt | 108 ++ .../room/donation/LiveRoomDonationMessage.kt | 10 + .../LiveRoomDonationMessageAdapter.kt | 62 + .../donation/LiveRoomDonationMessageDialog.kt | 115 ++ ...LiveRoomDonationMessageDiffUtilCallback.kt | 24 + .../LiveRoomDonationMessageViewModel.kt | 96 ++ .../LiveRoomDonationRankingAdapter.kt | 146 ++ .../donation/LiveRoomDonationRankingDialog.kt | 87 + .../room/donation/LiveRoomDonationRequest.kt | 10 + .../live/room/info/GetRoomInfoResponse.kt | 26 + .../sodalive/live/room/info/LiveRoomMember.kt | 19 + .../room/kick_out/LiveRoomKickOutRequest.kt | 8 + .../profile/GetLiveRoomUserProfileResponse.kt | 20 + .../LiveRoomMemberResponseDiffUtilCallback.kt | 27 + .../room/profile/LiveRoomProfileAdapter.kt | 248 +++ .../room/profile/LiveRoomProfileDialog.kt | 71 + .../live/room/profile/LiveRoomProfileItem.kt | 201 +++ .../profile/LiveRoomProfileListAdapter.kt | 86 + .../room/profile/LiveRoomUserProfileDialog.kt | 192 +++ .../room/update/LiveRoomInfoEditDialog.kt | 88 ++ .../sodalive/report/ProfileReportDialog.kt | 50 + .../co/vividnext/sodalive/report/ReportApi.kt | 15 + .../sodalive/report/ReportRepository.kt | 5 + .../sodalive/report/ReportRequest.kt | 19 + .../sodalive/report/UserReportDialog.kt | 58 + .../user/CreatorFollowRequestRequest.kt | 5 + .../kr/co/vividnext/sodalive/user/UserApi.kt | 25 + .../vividnext/sodalive/user/UserRepository.kt | 11 + .../main/res/drawable-xxhdpi/btn_follow.png | Bin 0 -> 4311 bytes .../res/drawable-xxhdpi/btn_following.png | Bin 0 -> 4465 bytes .../res/drawable-xxhdpi/btn_message_send.png | Bin 0 -> 978 bytes app/src/main/res/drawable-xxhdpi/ic_alarm.png | Bin 0 -> 781 bytes .../res/drawable-xxhdpi/ic_alarm_selected.png | Bin 0 -> 804 bytes .../res/drawable-xxhdpi/ic_badge_manager.png | Bin 0 -> 2088 bytes .../res/drawable-xxhdpi/ic_bottom_white.png | Bin 0 -> 401 bytes app/src/main/res/drawable-xxhdpi/ic_crown.png | Bin 0 -> 634 bytes .../main/res/drawable-xxhdpi/ic_crown_1.png | Bin 0 -> 2132 bytes .../main/res/drawable-xxhdpi/ic_crown_2.png | Bin 0 -> 2340 bytes .../main/res/drawable-xxhdpi/ic_crown_3.png | Bin 0 -> 2254 bytes .../main/res/drawable-xxhdpi/ic_donation.png | Bin 0 -> 4898 bytes .../ic_donation_message_list.png | Bin 0 -> 3606 bytes .../drawable-xxhdpi/ic_donation_status.png | Bin 0 -> 6819 bytes .../res/drawable-xxhdpi/ic_donation_white.png | Bin 0 -> 891 bytes app/src/main/res/drawable-xxhdpi/ic_edit.png | Bin 0 -> 491 bytes .../main/res/drawable-xxhdpi/ic_kick_out.png | Bin 0 -> 2522 bytes .../res/drawable-xxhdpi/ic_message_send.png | Bin 0 -> 787 bytes .../main/res/drawable-xxhdpi/ic_mic_off.png | Bin 0 -> 645 bytes .../main/res/drawable-xxhdpi/ic_mic_on.png | Bin 0 -> 564 bytes app/src/main/res/drawable-xxhdpi/ic_mute.png | Bin 0 -> 7811 bytes app/src/main/res/drawable-xxhdpi/ic_noti.png | Bin 0 -> 6381 bytes .../res/drawable-xxhdpi/ic_notice_normal.png | Bin 0 -> 2695 bytes .../drawable-xxhdpi/ic_notice_selected.png | Bin 0 -> 2262 bytes .../res/drawable-xxhdpi/ic_request_speak.png | Bin 0 -> 5076 bytes .../drawable-xxhdpi/ic_seemore_vertical.png | Bin 0 -> 198 bytes app/src/main/res/drawable-xxhdpi/ic_share.png | Bin 0 -> 950 bytes .../res/drawable-xxhdpi/ic_speaker_off.png | Bin 0 -> 5185 bytes .../res/drawable-xxhdpi/ic_speaker_on.png | Bin 0 -> 5950 bytes .../res/drawable-xxhdpi/img_noti_mute.png | Bin 0 -> 23305 bytes .../bg_bottom_round_corner_10_222222.xml | 10 + .../bg_bottom_round_corner_4_7_2b2635.xml | 10 + .../main/res/drawable/bg_circle_4999e3.xml | 9 + .../res/drawable/bg_circle_6f3dec_9970ff.xml | 9 + .../res/drawable/bg_circle_9f9f9f_bbbbbb.xml | 9 + .../res/drawable/bg_circle_9f9f9f_dcdcdc.xml | 9 + .../res/drawable/bg_circle_e5a578_c67e4a.xml | 9 + .../res/drawable/bg_circle_e6a77a_c67e4a.xml | 9 + .../res/drawable/bg_circle_ffdc00_fdca2f.xml | 9 + .../res/drawable/bg_circle_ffdc00_ffb600.xml | 9 + .../res/drawable/bg_circle_ffffff_9f9f9f.xml | 9 + .../bg_round_corner_10_232323_eeeeee.xml | 8 + .../drawable/bg_round_corner_10_99525252.xml | 8 + .../bg_round_corner_13_3_33ffffff_ffffff.xml | 8 + .../drawable/bg_round_corner_13_3_ffffff.xml | 8 + ...g_round_corner_13_3_transparent_9970ff.xml | 8 + ...g_round_corner_13_3_transparent_bbbbbb.xml | 8 + ...g_round_corner_13_3_transparent_ff5c49.xml | 8 + .../bg_round_corner_15_transparent_bbbbbb.xml | 8 + .../bg_round_corner_16_7_cc555555.xml | 8 + .../bg_round_corner_23_3_3e1b93_9970ff.xml | 8 + .../drawable/bg_round_corner_23_3_555555.xml | 8 + ...g_round_corner_23_3_transparent_9970ff.xml | 8 + .../res/drawable/bg_round_corner_2_4999e3.xml | 8 + .../drawable/bg_round_corner_3_3_99000000.xml | 8 + .../drawable/bg_round_corner_3_3_999970ff.xml | 8 + ...g_round_corner_46_7_transparent_9970ff.xml | 8 + .../drawable/bg_round_corner_4_7_3d2a6c.xml | 8 + .../drawable/bg_round_corner_5_3_333333.xml | 8 + ...bg_round_corner_5_3_transparent_909090.xml | 8 + .../drawable/bg_round_corner_6_7_303030.xml | 8 + .../bg_round_corner_6_7_339970ff_9970ff.xml | 8 + .../drawable/bg_round_corner_6_7_88333333.xml | 8 + .../drawable/bg_round_corner_6_7_c25264.xml | 8 + .../drawable/bg_round_corner_6_7_e62d7390.xml | 8 + .../drawable/bg_round_corner_6_7_e64d6aa4.xml | 8 + .../drawable/bg_round_corner_6_7_e6548f7d.xml | 8 + .../drawable/bg_round_corner_6_7_e659548f.xml | 8 + .../drawable/bg_round_corner_6_7_e6d38c38.xml | 8 + .../drawable/bg_round_corner_6_7_e6d85e37.xml | 8 + .../res/drawable/bg_round_corner_8_222222.xml | 8 + .../res/drawable/bg_round_corner_8_2b2635.xml | 8 + .../bg_top_round_corner_10_222222.xml | 10 + .../bg_top_round_corner_4_7_2b2635.xml | 10 + .../main/res/layout/activity_live_room.xml | 496 ++++++ app/src/main/res/layout/dialog_live_room.xml | 73 + .../res/layout/dialog_live_room_donation.xml | 203 +++ .../dialog_live_room_donation_message.xml | 70 + .../dialog_live_room_donation_ranking.xml | 123 ++ .../layout/dialog_live_room_info_update.xml | 176 +++ .../res/layout/dialog_live_room_profile.xml | 73 + .../layout/dialog_live_room_user_profile.xml | 241 +++ .../main/res/layout/dialog_profile_report.xml | 55 + .../main/res/layout/dialog_user_report.xml | 136 ++ .../main/res/layout/item_live_room_chat.xml | 106 ++ .../item_live_room_donation_message.xml | 57 + .../item_live_room_donation_ranking.xml | 100 ++ .../item_live_room_donation_status_chat.xml | 17 + .../res/layout/item_live_room_join_chat.xml | 17 + .../layout/item_live_room_list_profile.xml | 92 ++ .../res/layout/item_live_room_profile.xml | 57 + .../layout/item_live_room_profile_header.xml | 46 + .../layout/item_live_room_profile_manager.xml | 41 + .../layout/item_live_room_profile_master.xml | 41 + .../audio_content_detail_creator_menu.xml | 13 + .../menu/audio_content_detail_user_menu.xml | 8 + app/src/main/res/menu/review_option_menu.xml | 8 + .../res/menu/user_profile_option_menu.xml | 18 + .../res/menu/user_profile_option_menu_2.xml | 18 + app/src/main/res/values/colors.xml | 31 +- 151 files changed, 7972 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt create mode 100644 app/src/main/res/drawable-xxhdpi/btn_follow.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_following.png create mode 100644 app/src/main/res/drawable-xxhdpi/btn_message_send.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_alarm.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_badge_manager.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_bottom_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_crown.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_crown_1.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_crown_2.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_crown_3.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_donation.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_donation_message_list.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_donation_status.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_donation_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_edit.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_kick_out.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_message_send.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_mic_off.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_mic_on.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_mute.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_noti.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notice_normal.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notice_selected.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_request_speak.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_share.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_speaker_off.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_speaker_on.png create mode 100644 app/src/main/res/drawable-xxhdpi/img_noti_mute.png create mode 100644 app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml create mode 100644 app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml create mode 100644 app/src/main/res/drawable/bg_circle_4999e3.xml create mode 100644 app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml create mode 100644 app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml create mode 100644 app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml create mode 100644 app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml create mode 100644 app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml create mode 100644 app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml create mode 100644 app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_99525252.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_23_3_555555.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_2_4999e3.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_333333.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_303030.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_8_222222.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_8_2b2635.xml create mode 100644 app/src/main/res/drawable/bg_top_round_corner_10_222222.xml create mode 100644 app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml create mode 100644 app/src/main/res/layout/activity_live_room.xml create mode 100644 app/src/main/res/layout/dialog_live_room.xml create mode 100644 app/src/main/res/layout/dialog_live_room_donation.xml create mode 100644 app/src/main/res/layout/dialog_live_room_donation_message.xml create mode 100644 app/src/main/res/layout/dialog_live_room_donation_ranking.xml create mode 100644 app/src/main/res/layout/dialog_live_room_info_update.xml create mode 100644 app/src/main/res/layout/dialog_live_room_profile.xml create mode 100644 app/src/main/res/layout/dialog_live_room_user_profile.xml create mode 100644 app/src/main/res/layout/dialog_profile_report.xml create mode 100644 app/src/main/res/layout/dialog_user_report.xml create mode 100644 app/src/main/res/layout/item_live_room_chat.xml create mode 100644 app/src/main/res/layout/item_live_room_donation_message.xml create mode 100644 app/src/main/res/layout/item_live_room_donation_ranking.xml create mode 100644 app/src/main/res/layout/item_live_room_donation_status_chat.xml create mode 100644 app/src/main/res/layout/item_live_room_join_chat.xml create mode 100644 app/src/main/res/layout/item_live_room_list_profile.xml create mode 100644 app/src/main/res/layout/item_live_room_profile.xml create mode 100644 app/src/main/res/layout/item_live_room_profile_header.xml create mode 100644 app/src/main/res/layout/item_live_room_profile_manager.xml create mode 100644 app/src/main/res/layout/item_live_room_profile_master.xml create mode 100644 app/src/main/res/menu/audio_content_detail_creator_menu.xml create mode 100644 app/src/main/res/menu/audio_content_detail_user_menu.xml create mode 100644 app/src/main/res/menu/review_option_menu.xml create mode 100644 app/src/main/res/menu/user_profile_option_menu.xml create mode 100644 app/src/main/res/menu/user_profile_option_menu_2.xml diff --git a/app/build.gradle b/app/build.gradle index 9d8c574..e13d187 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,8 @@ android { buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"' buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"' + buildConfigField 'String', 'AGORA_APP_ID', '"4566e5b98f434b9eabb63435f794a1f9"' + buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"e89e30f9ee584fda9454db3c0e387632"' } debug { @@ -59,6 +61,8 @@ android { buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"' buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"' + buildConfigField 'String', 'AGORA_APP_ID', '"d28c80855d314a599cd7c15280920699"' + buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"29ef33b7c37e4b80b74af9a6e9b2af5e"' } } compileOptions { @@ -131,4 +135,8 @@ dependencies { // bootpay implementation "io.github.bootpay:android:4.3.4" + + // agora + implementation "io.agora.rtc:voice-sdk:4.1.0-1" + implementation 'io.agora.rtm:rtm-sdk:1.5.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0aa820..4036e32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ + { + override fun onSuccess(p0: Void?) { + Logger.e("sendMessage - onSuccess") + } + + override fun onFailure(p0: ErrorInfo) { + Logger.e("sendMessage fail - ${p0.errorCode}") + Logger.e("sendMessage fail - ${p0.errorDescription}") + } + } + ) + } + + fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) { + rtcEngine!!.joinChannel( + rtcToken, + channelName, + "", + uid + ) + } + + fun createRtmChannelAndLogin( + uid: String, + rtmToken: String, + channelName: String, + rtmChannelListener: RtmChannelListener, + rtmChannelJoinSuccess: () -> Unit, + rtmChannelJoinFail: () -> Unit + ) { + rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener) + rtmClient!!.login( + rtmToken, + uid, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + rtmChannel!!.join(object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("rtmChannel join - onSuccess") + rtmChannelJoinSuccess() + } + + override fun onFailure(p0: ErrorInfo?) { + rtmChannelJoinFail() + } + }) + } + + override fun onFailure(p0: ErrorInfo?) { + } + } + ) + } + + fun sendRawMessageToGroup( + rawMessage: ByteArray, + onSuccess: (() -> Unit)? = null, + onFailure: (() -> Unit)? = null + ) { + val message = rtmClient!!.createMessage() + message.rawMessage = rawMessage + rtmChannel!!.sendMessage( + message, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("sendMessage - onSuccess") + onSuccess?.invoke() + } + + override fun onFailure(p0: ErrorInfo) { + Logger.e("sendMessage fail - ${p0.errorCode}") + Logger.e("sendMessage fail - ${p0.errorDescription}") + onFailure?.invoke() + } + } + ) + } + + fun setClientRole(role: Int) { + rtcEngine!!.setClientRole(role) + } + + fun muteLocalAudioStream(muted: Boolean) { + rtcEngine?.muteLocalAudioStream(muted) + } + + fun muteAllRemoteAudioStreams(mute: Boolean) { + rtcEngine?.muteAllRemoteAudioStreams(mute) + } + + fun sendRawMessageToPeer( + receiverUid: String, + requestType: LiveRoomRequestType, + onSuccess: () -> Unit + ) { + val option = SendMessageOptions() + + val message = rtmClient!!.createMessage() + message.rawMessage = requestType.toString().toByteArray() + + rtmClient!!.sendMessageToPeer( + receiverUid, + message, + option, + object : ResultCallback { + override fun onSuccess(aVoid: Void?) { + onSuccess() + } + + override fun onFailure(errorInfo: ErrorInfo) { + } + } + ) + } + + fun rtmChannelIsNull(): Boolean { + return rtmChannel == null + } + + fun getConnectionState(): Int { + return rtcEngine!!.connectionState + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index 9dde486..81c1496 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -25,4 +25,6 @@ object Constants { const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" const val EXTRA_CONTENT_ID = "extra_content_id" + + const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt new file mode 100644 index 0000000..2242bf8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt @@ -0,0 +1,90 @@ +package kr.co.vividnext.sodalive.common + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.live.LiveViewModel +import kr.co.vividnext.sodalive.live.room.LiveRoomActivity +import org.koin.android.ext.android.inject + +class SodaLiveService : Service() { + + private val liveViewModel: LiveViewModel by inject() + + var roomId: Long = 0 + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val content = intent?.getStringExtra("content") ?: "라이브 진행중" + roomId = intent?.getLongExtra("roomId", 0) ?: 0L + updateNotification(content) + return START_STICKY + } + + private fun updateNotification(content: String) { + startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, createNotification(content)) + } + + private fun createNotification(content: String): Notification { + val notificationChannelId = "soda_live_service_foreground_service_channel" + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + notificationChannelId, + getString(R.string.app_name), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + + val intent = Intent(this, LiveRoomActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT + ) + + val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId) + .setSmallIcon(R.drawable.ic_noti) + .setContentTitle(getString(R.string.app_name)) + .setContentText(content) + .setOngoing(true) + .setSilent(true) + .setContentIntent(pendingIntent) + + return notificationBuilder.build() + } + + override fun onDestroy() { + liveViewModel.quitRoom(roomId) { } + super.onDestroy() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + stopSelf() + } + + companion object { + fun stopService(context: Context) { + val intent = Intent(context, SodaLiveService::class.java) + context.stopService(intent) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 5880722..036487d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -9,8 +9,10 @@ import kr.co.vividnext.sodalive.live.LiveRepository import kr.co.vividnext.sodalive.live.LiveViewModel import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository +import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel @@ -24,6 +26,8 @@ import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel import kr.co.vividnext.sodalive.network.TokenAuthenticator +import kr.co.vividnext.sodalive.report.ReportApi +import kr.co.vividnext.sodalive.report.ReportRepository import kr.co.vividnext.sodalive.settings.event.EventApi import kr.co.vividnext.sodalive.settings.event.EventRepository import kr.co.vividnext.sodalive.settings.terms.TermsApi @@ -76,13 +80,14 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { .build() } - single { ApiBuilder().build(get(), UserApi::class.java) } - single { ApiBuilder().build(get(), TermsApi::class.java) } - single { ApiBuilder().build(get(), LiveApi::class.java) } - single { ApiBuilder().build(get(), EventApi::class.java) } - single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } - single { ApiBuilder().build(get(), AuthApi::class.java) } single { ApiBuilder().build(get(), CanApi::class.java) } + single { ApiBuilder().build(get(), AuthApi::class.java) } + single { ApiBuilder().build(get(), UserApi::class.java) } + single { ApiBuilder().build(get(), LiveApi::class.java) } + single { ApiBuilder().build(get(), TermsApi::class.java) } + single { ApiBuilder().build(get(), EventApi::class.java) } + single { ApiBuilder().build(get(), ReportApi::class.java) } + single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } } private val viewModelModule = module { @@ -100,17 +105,20 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { LiveRoomCreateViewModel(get()) } viewModel { LiveTagViewModel(get()) } viewModel { LiveRoomEditViewModel(get()) } + viewModel { LiveRoomViewModel(get(), get(), get()) } + viewModel { LiveRoomDonationMessageViewModel(get()) } } private val repositoryModule = module { factory { UserRepository(get()) } factory { TermsRepository(get()) } - factory { LiveRepository(get()) } + factory { LiveRepository(get(), get()) } factory { EventRepository(get()) } factory { LiveRecommendRepository(get()) } factory { AuthRepository(get()) } factory { CanRepository(get()) } factory { LiveTagRepository(get()) } + factory { ReportRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt new file mode 100644 index 0000000..03cad5d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import com.google.gson.annotations.SerializedName + +data class MemberBlockRequest(@SerializedName("blockMemberId") val blockMemberId: Long) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt index 272b69f..a7bff5d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt @@ -8,15 +8,25 @@ import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse import kr.co.vividnext.sodalive.live.room.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest import kr.co.vividnext.sodalive.live.room.LiveRoomStatus +import kr.co.vividnext.sodalive.live.room.SetManagerOrSpeakerOrAudienceRequest import kr.co.vividnext.sodalive.live.room.StartLiveRequest import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse import kr.co.vividnext.sodalive.live.room.create.GetRecentRoomInfoResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse +import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest +import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse +import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest +import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse import kr.co.vividnext.sodalive.live.room.tag.GetLiveTagResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.HTTP import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST @@ -93,4 +103,83 @@ interface LiveApi { @Part("request") request: RequestBody?, @Header("Authorization") authHeader: String ): Single> + + @GET("/live/room/info/{id}") + fun getRoomInfo( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/live/room/donation-message") + fun getDonationMessageList( + @Query("roomId") roomId: Long, + @Header("Authorization") authHeader: String + ): Single>> + + @HTTP(method = "DELETE", path = "/live/room/donation-message", hasBody = true) + fun deleteDonationMessage( + @Body request: DeleteLiveRoomDonationMessage, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/live/room/{room_id}/profile/{user_id}") + fun getUserProfile( + @Path("room_id") roomId: Long, + @Path("user_id") userId: Long, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/live/room/{id}/donation-total") + fun getDonationTotal( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/live/room/info/set/speaker") + fun setSpeaker( + @Body request: SetManagerOrSpeakerOrAudienceRequest, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/live/room/info/set/listener") + fun setListener( + @Body request: SetManagerOrSpeakerOrAudienceRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/live/room/kick-out") + fun kickOut( + @Body request: LiveRoomKickOutRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/live/room/donation") + fun donation( + @Body request: LiveRoomDonationRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/live/room/donation/refund/{id}") + fun refundDonation( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/live/room/quit") + fun quitRoom( + @Query("id") roomId: Long, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/live/room/info/set/manager") + fun setManager( + @Body request: SetManagerOrSpeakerOrAudienceRequest, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/live/room/{id}/donation-list") + fun donationStatus( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt index 31c8eaa..8eb9013 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt @@ -34,6 +34,7 @@ import kr.co.vividnext.sodalive.live.recommend.RecommendLiveAdapter import kr.co.vividnext.sodalive.live.recommend_channel.LiveRecommendChannelAdapter import kr.co.vividnext.sodalive.live.reservation.LiveReservationAdapter import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity +import kr.co.vividnext.sodalive.live.room.LiveRoomActivity import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment @@ -65,7 +66,13 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { + val roomId = it.data?.getLongExtra(Constants.EXTRA_ROOM_ID, 0) + val channelName = it.data?.getStringExtra(Constants.EXTRA_ROOM_CHANNEL_NAME) refreshSummary() + + if (channelName != null) { + enterLiveRoom(roomId = roomId!!) + } } } } @@ -271,7 +278,7 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl liveNowAdapter = LiveNowAdapter { val detailFragment = LiveRoomDetailFragment( it.roomId, - onClickParticipant = {}, + onClickParticipant = { enterLiveRoom(it.roomId) }, onClickReservation = {}, onClickModify = {}, onClickStart = {}, @@ -326,7 +333,10 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl recyclerView.visibility = View.GONE binding.layoutLiveNow.tvAllView.visibility = View.GONE binding.layoutLiveNow.llNoItems.visibility = View.VISIBLE - binding.layoutLiveNow.tvMakeRoom.setOnClickListener {} + binding.layoutLiveNow.tvMakeRoom.setOnClickListener { + val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java) + activityResultLauncher.launch(intent) + } recyclerView.requestLayout() binding.layoutLiveNow.llNoItems.requestLayout() @@ -410,7 +420,11 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl recyclerView.visibility = View.GONE binding.layoutLiveReservation.tvAllView.visibility = View.GONE binding.layoutLiveReservation.llNoItems.visibility = View.VISIBLE - binding.layoutLiveReservation.tvMakeRoom.setOnClickListener {} + binding.layoutLiveReservation.tvMakeRoom.setOnClickListener { + val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java) + intent.putExtra(Constants.EXTRA_LIVE_TIME_NOW, false) + activityResultLauncher.launch(intent) + } recyclerView.requestLayout() binding.layoutLiveReservation.llNoItems.requestLayout() @@ -468,6 +482,11 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl private fun startLive(roomId: Long) { val onEnterRoomSuccess = { viewModel.getSummary() + requireActivity().runOnUiThread { + val intent = Intent(requireContext(), LiveRoomActivity::class.java) + intent.putExtra(Constants.EXTRA_ROOM_ID, roomId) + startActivity(intent) + } } viewModel.startLive(roomId, onEnterRoomSuccess) @@ -553,4 +572,90 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl } ) } + + fun enterLiveRoom(roomId: Long) { + val onEnterRoomSuccess = { + requireActivity().runOnUiThread { + val intent = Intent(requireContext(), LiveRoomActivity::class.java) + intent.putExtra(Constants.EXTRA_ROOM_ID, roomId) + startActivity(intent) + } + } + + viewModel.getRoomDetail(roomId) { + if (it.channelName != null) { + if (it.manager.id == SharedPreferenceManager.userId) { + handler.postDelayed({ + viewModel.enterRoom(roomId, onEnterRoomSuccess) + }, 300) + } else if (it.price == 0 || it.isPaid) { + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = requireActivity(), + layoutInflater = layoutInflater, + can = 0, + confirmButtonClick = { password -> + viewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidth) + } else { + handler.postDelayed({ + viewModel.enterRoom(roomId, onEnterRoomSuccess) + }, 300) + } + } else { + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = requireActivity(), + layoutInflater = layoutInflater, + can = it.price, + confirmButtonClick = { password -> + handler.postDelayed({ + viewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + }, 300) + } + ).show(screenWidth) + } else { + LivePaymentDialog( + activity = requireActivity(), + layoutInflater = layoutInflater, + title = "${it.price.moneyFormat()}코인으로 입장", + desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.", + confirmButtonTitle = "결제 후 입장", + confirmButtonClick = { + handler.postDelayed({ + viewModel.enterRoom(roomId, onEnterRoomSuccess) + }, 300) + }, + cancelButtonTitle = "취소", + cancelButtonClick = {} + ).show(screenWidth) + } + } + } else { + val detailFragment = LiveRoomDetailFragment( + it.roomId, + onClickParticipant = {}, + onClickReservation = { reservationRoom(it.roomId) }, + onClickModify = { roomDetailResponse -> modifyLive(roomDetailResponse) }, + onClickStart = { startLive(it.roomId) }, + onClickCancel = { cancelLive(it.roomId) } + ) + if (detailFragment.isAdded) return@getRoomDetail + + detailFragment.show( + requireActivity().supportFragmentManager, + detailFragment.tag + ) + } + } + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt index 7167f78..fec9412 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt @@ -7,14 +7,23 @@ import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest import kr.co.vividnext.sodalive.live.room.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest import kr.co.vividnext.sodalive.live.room.LiveRoomStatus +import kr.co.vividnext.sodalive.live.room.SetManagerOrSpeakerOrAudienceRequest import kr.co.vividnext.sodalive.live.room.StartLiveRequest import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse +import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest +import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest +import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest +import kr.co.vividnext.sodalive.user.UserApi import okhttp3.MultipartBody import okhttp3.RequestBody import java.util.TimeZone -class LiveRepository(private val api: LiveApi) { +class LiveRepository( + private val api: LiveApi, + private val userApi: UserApi +) { fun roomList( dateString: String? = null, status: LiveRoomStatus, @@ -94,4 +103,109 @@ class LiveRepository(private val api: LiveApi) { request = request, authHeader = token ) + + fun getRoomInfo(roomId: Long, token: String) = api.getRoomInfo(roomId, authHeader = token) + + fun getDonationMessageList( + roomId: Long, + token: String + ) = api.getDonationMessageList(roomId = roomId, authHeader = token) + + fun deleteDonationMessage( + roomId: Long, + uuid: String, + token: String + ) = api.deleteDonationMessage( + request = DeleteLiveRoomDonationMessage( + roomId, + messageUUID = uuid + ), + authHeader = token + ) + + fun getUserProfile(roomId: Long, userId: Long, token: String) = api.getUserProfile( + roomId = roomId, + userId = userId, + authHeader = token + ) + + fun getTotalDonationCan( + roomId: Long, + token: String + ) = api.getDonationTotal(roomId, authHeader = token) + + fun setSpeaker(roomId: Long, userId: Long, token: String): Single> { + return api.setSpeaker( + request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId), + authHeader = token + ) + } + + fun setListener(roomId: Long, userId: Long, token: String): Single> { + return api.setListener( + request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId), + authHeader = token + ) + } + + fun kickOut(roomId: Long, userId: Long, token: String): Single> { + return api.kickOut( + request = LiveRoomKickOutRequest(roomId, userId), + authHeader = token + ) + } + + fun donation( + roomId: Long, + can: Int, + message: String, + token: String + ): Single> { + return api.donation( + request = LiveRoomDonationRequest( + roomId = roomId, + can = can, + message = message, + container = "aos" + ), + authHeader = token + ) + } + + fun refundDonation(roomId: Long, token: String): Single> { + return api.refundDonation( + id = roomId, + authHeader = token + ) + } + + fun quitRoom(roomId: Long, token: String): Single> { + return api.quitRoom(roomId = roomId, authHeader = token) + } + + fun setManager(roomId: Long, userId: Long, token: String) = api.setManager( + request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId), + authHeader = token, + ) + + fun creatorFollow( + creatorId: Long, + token: String + ) = userApi.creatorFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) + + fun creatorUnFollow( + creatorId: Long, + token: String + ) = userApi.creatorUnFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) + + fun donationStatus( + roomId: Long, + token: String + ) = api.donationStatus(roomId, authHeader = token) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt index 51c07d7..e950489 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt @@ -11,11 +11,11 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository import kr.co.vividnext.sodalive.live.recommend_channel.GetRecommendChannelResponse +import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest +import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse import kr.co.vividnext.sodalive.live.room.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest import kr.co.vividnext.sodalive.live.room.LiveRoomStatus -import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest -import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse import kr.co.vividnext.sodalive.live.room.StartLiveRequest import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.settings.event.EventItem @@ -245,6 +245,37 @@ class LiveViewModel( } } + fun quitRoom(roomId: Long, onSuccess: () -> Unit) { + _isLoading.value = true + compositeDisposable.add( + repository.quitRoom(roomId, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + onSuccess() + _isLoading.value = false + } else { + _isLoading.value = false + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + fun startLive(roomId: Long, onEnterRoomSuccess: () -> Unit) { _isLoading.value = true compositeDisposable.add( @@ -313,7 +344,7 @@ class LiveViewModel( ) } - fun enterRoom(roomId: Long, onSuccess: () -> Unit, password: Int? = null) { + fun enterRoom(roomId: Long, onSuccess: () -> Unit, password: String? = null) { _isLoading.value = true val request = EnterOrQuitLiveRoomRequest(roomId, password = password) compositeDisposable.add( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt index c78db54..2b130e0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt @@ -5,5 +5,5 @@ import com.google.gson.annotations.SerializedName data class EnterOrQuitLiveRoomRequest( @SerializedName("roomId") val roomId: Long, @SerializedName("container") val container: String = "aos", - @SerializedName("password") val password: Int? = null + @SerializedName("password") val password: String? = null ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt new file mode 100644 index 0000000..de2ec4f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -0,0 +1,1404 @@ +package kr.co.vividnext.sodalive.live.room + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.app.Service +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Spannable +import android.text.SpannableString +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import com.github.dhaval2404.imagepicker.ImagePicker +import com.google.gson.Gson +import com.orhanobut.logger.Logger +import io.agora.rtc2.ClientRoleOptions +import io.agora.rtc2.IRtcEngineEventHandler +import io.agora.rtc2.RtcConnection +import io.agora.rtm.RtmChannelAttribute +import io.agora.rtm.RtmChannelListener +import io.agora.rtm.RtmChannelMember +import io.agora.rtm.RtmClientListener +import io.agora.rtm.RtmMessage +import io.agora.rtm.RtmMessageType +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.agora.Agora +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.RealPathUtil +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.SodaLiveService +import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomBinding +import kr.co.vividnext.sodalive.dialog.LiveDialog +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatAdapter +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatRawMessage +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatRawMessageType +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationChat +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationStatusChat +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomJoinChat +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomNormalChat +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRankingDialog +import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse +import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog +import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter +import kr.co.vividnext.sodalive.live.room.profile.LiveRoomUserProfileDialog +import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog +import kr.co.vividnext.sodalive.report.ProfileReportDialog +import kr.co.vividnext.sodalive.report.ReportType +import kr.co.vividnext.sodalive.report.UserReportDialog +import kr.co.vividnext.sodalive.settings.notification.MemberRole +import org.koin.android.ext.android.inject +import java.util.regex.Pattern + +class LiveRoomActivity : BaseActivity(ActivityLiveRoomBinding::inflate) { + + private var roomId: Long = 0 + + private val viewModel: LiveRoomViewModel by inject() + private val donationMessageViewModel: LiveRoomDonationMessageViewModel by inject() + + private lateinit var speakerListAdapter: LiveRoomProfileListAdapter + private lateinit var loadingDialog: LoadingDialog + + private lateinit var imm: InputMethodManager + private val handler = Handler(Looper.getMainLooper()) + + private val chatAdapter = LiveRoomChatAdapter { userId -> + showLiveRoomUserProfileDialog(userId = userId) + } + private lateinit var layoutManager: LinearLayoutManager + + private lateinit var agora: Agora + private lateinit var roomDialog: LiveRoomDialog + private lateinit var roomProfileDialog: LiveRoomProfileDialog + private lateinit var roomInfoEditDialog: LiveRoomInfoEditDialog + private lateinit var roomUserProfileDialog: LiveRoomUserProfileDialog + + private var isSpeakerMute = false + private var isMicrophoneMute = false + private var isSpeaker = false + private var isSpeakerFold = false + private var isAvailableDonation = false + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onClickQuit() + } + } + + private val imageResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + // Image Uri will not be null for RESULT_OK + val fileUri = data?.data!! + roomInfoEditDialog.setCoverImageUri(fileUri) + } else if (resultCode == ImagePicker.RESULT_ERROR) { + Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + agora = Agora( + context = this, + rtcEventHandler = rtcEventHandler, + rtmClientListener = rtmClientListener + ) + + super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + viewModel.getRealPathFromURI = { + RealPathUtil.getRealPath(applicationContext, it) + } + this.roomId = intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0) + + if (roomId <= 0) { + showToast("해당하는 라이브가 없습니다.") + finish() + return + } + + viewModel.getMemberCan() + viewModel.getRoomInfo(roomId) + } + + override fun onStart() { + super.onStart() + + if (this::layoutManager.isInitialized) { + layoutManager.scrollToPosition(chatAdapter.itemCount - 1) + } + + if ( + viewModel.isRoomInfoInitialized() && + agora.getConnectionState() == + RtcConnection.CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED.ordinal + ) { + val userId = SharedPreferenceManager.userId + agora.joinRtcChannel( + uid = userId.toInt(), + rtcToken = viewModel.roomInfoResponse.rtcToken, + channelName = viewModel.roomInfoResponse.channelName + ) + } + } + + override fun setupView() { + bindData() + + loadingDialog = LoadingDialog(this, layoutInflater) + imm = getSystemService( + Service.INPUT_METHOD_SERVICE + ) as InputMethodManager + + roomDialog = LiveRoomDialog(this, layoutInflater) + roomInfoEditDialog = LiveRoomInfoEditDialog( + activity = this, + layoutInflater = layoutInflater, + onClickImagePicker = { + ImagePicker.with(this) + .crop() + .galleryOnly() + .galleryMimeTypes( // Exclude gif images + mimeTypes = arrayOf( + "image/png", + "image/jpg", + "image/jpeg" + ) + ) + .createIntent { imageResult.launch(it) } + } + ) + roomProfileDialog = LiveRoomProfileDialog( + layoutInflater = layoutInflater, + activity = this, + roomInfoLiveData = viewModel.roomInfoLiveData, + isStaff = { + viewModel.isEqualToManagerId( + accountId = SharedPreferenceManager.userId.toInt() + ) + }, + onClickInviteSpeaker = { accountId -> + if (speakerListAdapter.itemCount <= 9) { + inviteSpeaker(accountId) + } else { + showToast("스피커 정원이 초과했습니다.") + } + }, + onClickChangeListener = { accountId -> + if (accountId == SharedPreferenceManager.userId) { + handler.post { + viewModel.setListener( + roomId, + SharedPreferenceManager.userId + ) { + setAudience() + viewModel.getRoomInfo(roomId) + } + } + + return@LiveRoomProfileDialog + } + + changeListenerMessage(accountId) + }, + onClickKickOut = { + LiveDialog( + activity = this, + layoutInflater = layoutInflater, + title = "내보내기", + desc = "${viewModel.getUserNickname(it.toInt())}님을 내보내시겠어요?", + confirmButtonTitle = "내보내기", + confirmButtonClick = { kickOut(it) }, + cancelButtonTitle = "취소", + cancelButtonClick = {} + ).show(screenWidth) + }, + onClickProfile = { + showLiveRoomUserProfileDialog(userId = it) + } + ) + + roomUserProfileDialog = LiveRoomUserProfileDialog( + activity = this, + userProfileLiveData = viewModel.userProfileLiveData, + layoutInflater = layoutInflater, + isStaff = { + viewModel.isEqualToManagerId(it.toInt()) + }, + onClickSendMessage = { userId, nickname -> + }, + onClickSetManager = { + setManagerMessageToPeer(userId = it) + viewModel.setManager(roomId = roomId, userId = it) { + setManagerMessage() + showDialog( + content = "${viewModel.getUserNickname(it.toInt())}님을 스탭으로 지정했습니다." + ) + } + }, + onClickReleaseManager = { + changeListenerMessage(it, isFromManager = true) + }, + onClickFollow = { + viewModel.creatorFollow( + creatorId = it, + roomId = roomId, + isGetUserProfile = true + ) + }, + onClickUnFollow = { + viewModel.creatorUnFollow( + creatorId = it, + roomId = roomId, + isGetUserProfile = true + ) + }, + onClickInviteSpeaker = { + if (speakerListAdapter.itemCount <= 9) { + inviteSpeaker(it) + } else { + showToast("스피커 정원이 초과했습니다.") + } + }, + onClickChangeListener = { changeListenerMessage(it) }, + onClickKickOut = { + LiveDialog( + activity = this, + layoutInflater = layoutInflater, + title = "내보내기", + desc = "${viewModel.getUserNickname(it.toInt())}님을 내보내시겠어요?", + confirmButtonTitle = "내보내기", + confirmButtonClick = { kickOut(it) }, + cancelButtonTitle = "취소", + cancelButtonClick = {} + ).show(screenWidth) + }, + onClickPopupMenu = { userId, nickname, isBlock, view -> + showOptionMenu( + this, + userId, + nickname, + isBlock, + view, + ) + } + ) + + binding.tvQuit.setOnClickListener { onClickQuit() } + binding.flMicrophoneMute.setOnClickListener { + microphoneMute() + if (isMicrophoneMute) { + binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_off) + binding.ivNotiMicrophoneMute.visibility = View.VISIBLE + } else { + binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on) + binding.ivNotiMicrophoneMute.visibility = View.GONE + } + } + binding.flSpeakerMute.setOnClickListener { + speakerMute() + if (isSpeakerMute) { + binding.ivSpeakerMute.setImageResource(R.drawable.ic_speaker_off) + } else { + binding.ivSpeakerMute.setImageResource(R.drawable.ic_speaker_on) + } + } + binding.ivSend.setOnClickListener { inputChat() } + binding.flDonation.setOnClickListener { + val dialog = LiveRoomDonationDialog( + this, + LayoutInflater.from(this) + ) { can, message -> + if (can > 0) { + donation(can, message) + } else { + showToast("1코인 이상 후원하실 수 있습니다.") + } + } + + dialog.show(screenWidth) + } + binding.ivNotification.setOnClickListener { viewModel.toggleShowNotice() } + binding.rlNotice.setOnClickListener { viewModel.toggleExpandNotice() } + binding.tvSpeakerFold.setOnClickListener { + isSpeakerFold = !isSpeakerFold + + if (isSpeakerFold) { + binding.rlSpeaker.visibility = View.VISIBLE + binding.rvSpeakers.visibility = View.VISIBLE + binding.tvSpeakerFold.text = "접기" + binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_live_detail_top, + 0, + 0, + 0 + ) + } else { + binding.rlSpeaker.visibility = View.GONE + binding.rvSpeakers.visibility = View.GONE + binding.tvSpeakerFold.text = "펼치기" + binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_live_detail_bottom, + 0, + 0, + 0 + ) + } + } + + binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() } + binding.llDonation.setOnClickListener { + LiveRoomDonationRankingDialog( + activity = this, + layoutInflater = layoutInflater, + viewModel = viewModel, + roomId = roomId + ).show() + } + + setupChatAdapter() + setupSpeakerListAdapter() + } + + override fun onDestroy() { + hideKeyboard { + viewModel.quitRoom(roomId) { + SodaLiveService.stopService(this) + agora.deInitAgoraEngine() + } + } + super.onDestroy() + } + + private fun showOptionMenu( + context: Context, + userId: Long, + nickname: String, + isBlock: Boolean, + v: View + ) { + val popup = PopupMenu(context, v) + val inflater = popup.menuInflater + + if (isBlock) { + inflater.inflate(R.menu.user_profile_option_menu_2, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_user_block -> { + viewModel.memberUnBlock(userId) + } + + R.id.menu_user_report -> { + showUserReportDialog(userId) + } + + R.id.menu_profile_report -> { + showProfileReportDialog(userId) + } + } + + true + } + } else { + inflater.inflate(R.menu.user_profile_option_menu, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_user_block -> { + showMemberBlockDialog(userId, nickname) + } + + R.id.menu_user_report -> { + showUserReportDialog(userId) + } + + R.id.menu_profile_report -> { + showProfileReportDialog(userId) + } + } + + true + } + } + + popup.show() + } + + private fun showMemberBlockDialog(userId: Long, nickname: String) { + val dialog = AlertDialog.Builder(this) + dialog.setTitle("사용자 차단") + dialog.setMessage( + "${nickname}님을 차단하시겠습니까?\n\n" + + "사용자를 차단하면 사용자는 아래 기능이 제한됩니다.\n" + + "- 내가 개설한 라이브 입장 불가\n" + + "- 나에게 메시지 보내기 불가\n" + + "- 내 채널의 팬Talk 작성불가" + ) + dialog.setPositiveButton("차단") { _, _ -> + roomUserProfileDialog.dismiss() + viewModel.memberBlock(userId) { + kickOut(userId) + } + } + dialog.setNegativeButton("취소") { _, _ -> } + dialog.show() + } + + private fun showUserReportDialog(userId: Long) { + val dialog = UserReportDialog(this, layoutInflater) { + viewModel.report( + type = ReportType.USER, + userId = userId, + reason = it + ) + } + + dialog.show(screenWidth) + } + + private fun showProfileReportDialog(userId: Long) { + val dialog = ProfileReportDialog(this, layoutInflater) { + viewModel.report( + type = ReportType.PROFILE, + userId = userId + ) + } + + dialog.show(screenWidth) + } + + private fun showLiveRoomUserProfileDialog(userId: Long) { + viewModel.getUserProfile(roomId = roomId, userId = userId) { + roomUserProfileDialog.show() + } + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + viewModel.isBgOn.observe(this) { + if (it) { + binding.ivCover.visibility = View.VISIBLE + binding.tvBgSwitch.text = "배경 ON" + binding.tvBgSwitch.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_9970ff + ) + ) + binding.tvBgSwitch + .setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_9970ff) + } else { + binding.ivCover.visibility = View.GONE + binding.tvBgSwitch.text = "배경 OFF" + binding.tvBgSwitch.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + binding.tvBgSwitch + .setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_bbbbbb) + } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(this) { + it?.let { showToast(it) } + } + + donationMessageViewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + donationMessageViewModel.toastLiveData.observe(this) { + it?.let { showToast(it) } + } + + viewModel.roomInfoLiveData.observe(this) { response -> + binding.tvTitle.text = response.title + binding.ivCover.load(response.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + } + + isAvailableDonation = response.isAvailableDonation + binding.flDonation.visibility = if (response.isAvailableDonation) { + View.VISIBLE + } else { + View.GONE + } + + if ( + response.managerId == SharedPreferenceManager.userId && + SharedPreferenceManager.role == MemberRole.CREATOR.name + ) { + binding.flDonationMessageList.visibility = View.VISIBLE + binding.flDonationMessageList.setOnClickListener { + LiveRoomDonationMessageDialog( + layoutInflater = LayoutInflater.from(this), + activity = this, + donationMessageListLiveData = donationMessageViewModel + .donationMessageListLiveData, + donationMessageCountLiveData = donationMessageViewModel + .donationMessageCountLiveData, + getDonationMessageList = { + donationMessageViewModel.getDonationMessageList(roomId = roomId) + }, + deleteDonationMessage = { + donationMessageViewModel.deleteDonationMessage( + roomId = roomId, + uuid = it + ) + }, + copyMessage = { + val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText(it, it)) + showToast("후원 메시지가 복사되었습니다.") + } + ).show() + } + } else { + binding.flDonationMessageList.visibility = View.GONE + } + + speakerListAdapter.managerId = response.managerId + speakerListAdapter.updateList(response.speakerList) + + if (response.managerId == SharedPreferenceManager.userId) { + binding.ivEdit.setOnClickListener { + roomInfoEditDialog.setRoomInfo(response.title, response.notice) + roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) + roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri -> + viewModel.editLiveRoomInfo( + response.roomId, + newTitle, + newContent, + newCoverImageUri, + onSuccess = { + binding.tvTitle.text = newTitle + setNoticeAndClickableUrl(binding.tvNotice, newContent) + + if (newCoverImageUri != null) { + binding.ivCover.load(newCoverImageUri) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + } + } + + agora.sendRawMessageToGroup( + rawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO, + message = "", + can = 0, + donationMessage = "" + ) + ).toByteArray() + ) + } + ) + } + + roomInfoEditDialog.show(screenWidth) + } + + binding.ivEdit.visibility = View.VISIBLE + binding.tvQuit.text = "라이브 종료" + + handler.postDelayed({ + binding.tvQuit.requestLayout() + binding.ivEdit.requestLayout() + }, 250) + } else { + binding.ivEdit.visibility = View.GONE + } + + binding.ivShare.setOnClickListener { + viewModel.shareRoomLink( + response.roomId, + response.isPrivateRoom, + response.password + ) { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, it) + + val shareIntent = Intent.createChooser(intent, "라이브 공유") + startActivity(shareIntent) + } + } + + binding.llViewUsers.setOnClickListener { roomProfileDialog.show() } + binding.tvParticipate.text = "${response.participantsCount}" + setNoticeAndClickableUrl(binding.tvNotice, response.notice) + + binding.tvCreatorNickname.text = response.managerNickname + binding.ivCreatorProfile.load(response.managerProfileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.ivCreatorProfile.setOnClickListener { + if (response.managerId != SharedPreferenceManager.userId) { + showLiveRoomUserProfileDialog(userId = response.managerId) + } + } + + if (response.isAvailableDonation) { + binding.ivCreatorFollow.visibility = View.VISIBLE + + if (response.isFollowingManager) { + binding.ivCreatorFollow.setImageResource(R.drawable.btn_following) + binding.ivCreatorFollow.setOnClickListener { + viewModel.creatorUnFollow( + creatorId = response.managerId, + roomId = roomId + ) + } + } else { + binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow) + binding.ivCreatorFollow.setOnClickListener { + viewModel.creatorFollow( + creatorId = response.managerId, + roomId = roomId + ) + } + } + } else { + binding.ivCreatorFollow.visibility = View.GONE + } + + if (agora.rtmChannelIsNull()) { + joinChannel(response) + } + } + + viewModel.isShowNotice.observe(this) { + if (it) { + binding.ivNotification.setImageResource(R.drawable.ic_notice_selected) + binding.rlNotice.visibility = View.VISIBLE + } else { + binding.ivNotification.setImageResource(R.drawable.ic_notice_normal) + binding.rlNotice.visibility = View.GONE + } + } + + viewModel.isExpandNotice.observe(this) { + binding.tvNotice.maxLines = if (it) { + Int.MAX_VALUE + } else { + 1 + } + } + + viewModel.totalDonationCan.observe(this) { + binding.tvTotalCan.text = it.moneyFormat() + } + } + + private fun setNoticeAndClickableUrl(textView: TextView, text: String) { + textView.text = text + + val spannable = SpannableString(text) + val pattern = Pattern.compile("https?://\\S+") + val matcher = pattern.matcher(spannable) + + while (matcher.find()) { + val start = matcher.start() + val end = matcher.end() + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + val url = spannable.subSequence(start, end).toString() + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } + } + spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + textView.text = spannable + textView.movementMethod = LinkMovementMethod.getInstance() + } + + private fun onClickQuit() { + hideKeyboard { + if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) { + LiveDialog( + activity = this, + layoutInflater = layoutInflater, + title = "라이브 종료", + desc = "라이브를 종료하시겠습니까?\n" + + "라이브를 종료하면 대화내용은\n" + + "저장되지 않고 사라집니다.\n" + + "참여자들 또한 라이브가 종료되어\n" + + "강제퇴장 됩니다.", + confirmButtonTitle = "예", + confirmButtonClick = { finish() }, + cancelButtonTitle = "아니오", + cancelButtonClick = {} + ).show(screenWidth) + } else { + LiveDialog( + activity = this, + layoutInflater = layoutInflater, + title = "라이브 나가기", + desc = "라이브에서 나가시겠습니까?", + confirmButtonTitle = "예", + confirmButtonClick = { finish() }, + cancelButtonTitle = "아니오", + cancelButtonClick = {} + ).show(screenWidth) + } + } + } + + private fun hideKeyboard(onAfterExecute: () -> Unit) { + handler.postDelayed({ + imm.hideSoftInputFromWindow( + window.decorView.applicationWindowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) + onAfterExecute() + }, 100) + } + + private fun setupChatAdapter() { + val rvChat = binding.rvChat + layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + layoutManager.stackFromEnd = true + rvChat.layoutManager = layoutManager + rvChat.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.top = 10f.dpToPx().toInt() + outRect.bottom = 10f.dpToPx().toInt() + } + }) + rvChat.adapter = chatAdapter + rvChat.setOnScrollChangeListener { _, _, _, _, _ -> + if (!rvChat.canScrollVertically(1)) { + binding.tvNewChat.visibility = View.GONE + } + } + + binding.tvNewChat.setOnClickListener { + binding.tvNewChat.visibility = View.GONE + layoutManager.scrollToPosition(chatAdapter.itemCount - 1) + } + } + + private fun setupSpeakerListAdapter() { + val rvSpeakers = binding.rvSpeakers + speakerListAdapter = LiveRoomProfileListAdapter() + + rvSpeakers.layoutManager = GridLayoutManager(applicationContext, 5) + rvSpeakers.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.top = 5f.dpToPx().toInt() + outRect.bottom = 5f.dpToPx().toInt() + } + }) + rvSpeakers.adapter = speakerListAdapter + } + + private fun inviteSpeaker(peerId: Long) { + agora.sendRawMessageToPeer( + receiverUid = peerId.toString(), + requestType = LiveRoomRequestType.INVITE_SPEAKER + ) { + handler.post { + showDialog(content = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요.") + } + } + } + + private fun setAudience() { + isSpeaker = false + isMicrophoneMute = false + agora.muteLocalAudioStream(false) + agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE) + handler.postDelayed({ + binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on) + binding.flMicrophoneMute.visibility = View.GONE + binding.ivNotiMicrophoneMute.visibility = View.GONE + speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt()) + }, 100) + } + + private fun setBroadcaster() { + isSpeaker = true + isMicrophoneMute = false + agora.muteLocalAudioStream(false) + agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER) + handler.postDelayed({ + binding.flMicrophoneMute.visibility = View.VISIBLE + binding.ivNotiMicrophoneMute.visibility = View.GONE + }, 100) + } + + private fun changeListenerMessage(peerId: Long, isFromManager: Boolean = false) { + agora.sendRawMessageToPeer( + receiverUid = peerId.toString(), + requestType = LiveRoomRequestType.CHANGE_LISTENER + ) { + if (isFromManager) { + viewModel.getRoomInfo(roomId) + setManagerMessage() + releaseManagerMessageToPeer(userId = peerId) + + handler.post { + showDialog( + content = "${viewModel.getUserNickname(peerId.toInt())}님을 스탭에서 해제했어요." + ) + } + } else { + handler.post { + showDialog( + content = "${viewModel.getUserNickname(peerId.toInt())}님을 리스너로 변경했어요." + ) + } + } + } + } + + private fun releaseManagerMessageToPeer(userId: Long) { + agora.sendRawMessageToPeer( + receiverUid = userId.toString(), + requestType = LiveRoomRequestType.RELEASE_MANAGER + ) {} + } + + private fun setManagerMessageToPeer(userId: Long) { + agora.sendRawMessageToPeer( + receiverUid = userId.toString(), + requestType = LiveRoomRequestType.SET_MANAGER + ) {} + } + + private fun setManagerMessage() { + agora.sendRawMessageToGroup( + rawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.SET_MANAGER, + message = "", + can = 0, + donationMessage = "" + ) + ).toByteArray() + ) + } + + private fun kickOut(userId: Long) { + viewModel.kickOut(roomId, userId) + agora.sendRawMessageToPeer( + receiverUid = userId.toString(), + requestType = LiveRoomRequestType.KICK_OUT + ) { + handler.post { + showDialog( + content = "${viewModel.getUserNickname(userId.toInt())}님을 내보냈습니다." + ) + } + } + } + + private fun showDialog( + content: String, + cancelTitle: String = "", + cancelAction: (() -> Unit)? = null, + confirmTitle: String = "", + confirmAction: (() -> Unit)? = null + ) { + roomDialog.setContent(content) + + if (cancelTitle.isNotBlank() && cancelAction != null) { + roomDialog.setCancel(cancelTitle, cancelAction) + } + + if (confirmTitle.isNotBlank() && confirmAction != null) { + roomDialog.setConfirm(confirmTitle, confirmAction) + } + + roomDialog.show(screenWidth) + } + + private fun microphoneMute() { + isMicrophoneMute = !isMicrophoneMute + agora.muteLocalAudioStream(isMicrophoneMute) + + if (isMicrophoneMute) { + speakerListAdapter.muteSpeakers.add(SharedPreferenceManager.userId.toInt()) + } else { + speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt()) + } + } + + private fun speakerMute() { + isSpeakerMute = !isSpeakerMute + agora.muteAllRemoteAudioStreams(isSpeakerMute) + } + + private fun inputChat() { + val nickname = viewModel.getUserNickname(SharedPreferenceManager.userId.toInt()) + val profileUrl = viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt()) + val rank = viewModel.getUserRank(SharedPreferenceManager.userId) + + if (binding.etChat.text.isNotBlank() && nickname.isNotBlank() && profileUrl.isNotBlank()) { + val message = binding.etChat.text.toString() + chatAdapter.items.add( + LiveRoomNormalChat( + userId = SharedPreferenceManager.userId, + profileUrl = profileUrl, + nickname = nickname, + rank = rank, + chat = message + ) + ) + invalidateChat() + + agora.inputChat(message) + binding.etChat.setText("") + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun invalidateChat() { + chatAdapter.notifyDataSetChanged() + val lastVisiblePosition = layoutManager + .findLastVisibleItemPosition() + val itemTotalCount = chatAdapter.itemCount - 1 + + if ( + itemTotalCount > 0 && + lastVisiblePosition > itemTotalCount - 5 + ) { + layoutManager + .scrollToPosition(chatAdapter.itemCount - 1) + binding.tvNewChat.visibility = View.GONE + } else { + binding.tvNewChat.visibility = View.VISIBLE + } + } + + private fun donation(can: Int, message: String) { + val rawMessage = "${can}캔을 후원하셨습니다.\uD83D\uDCB0\uD83E\uDE99" + val donationRawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.DONATION, + message = rawMessage, + can = can, + donationMessage = message + ) + ) + + viewModel.donation(roomId, can, message) { + agora.sendRawMessageToGroup( + rawMessage = donationRawMessage.toByteArray(), + onSuccess = { + handler.post { + val nickname = + viewModel.getUserNickname(SharedPreferenceManager.userId.toInt()) + val profileUrl = + viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt()) + chatAdapter.items.add( + LiveRoomDonationChat( + profileUrl, + nickname, + rawMessage, + can, + donationMessage = message + ) + ) + invalidateChat() + viewModel.addDonationCan(can) + } + }, + onFailure = { + viewModel.refundDonation(roomId) + } + ) + } + } + + private fun joinChannel(roomInfo: GetRoomInfoResponse) { + val userId = SharedPreferenceManager.userId + agora.joinRtcChannel( + uid = userId.toInt(), + rtcToken = roomInfo.rtcToken, + channelName = roomInfo.channelName + ) + + agora.createRtmChannelAndLogin( + uid = userId.toString(), + rtmToken = roomInfo.rtmToken, + channelName = roomInfo.channelName, + rtmChannelListener = object : RtmChannelListener { + override fun onMemberCountUpdated(i: Int) { + Logger.e("onMemberCountUpdated: $i") + } + + override fun onAttributesUpdated(list: List?) {} + + @SuppressLint("NotifyDataSetChanged") + override fun onMessageReceived(message: RtmMessage, fromMember: RtmChannelMember) { + Logger.e("onMessageReceived - message: ${message.text}") + Logger.e("onMessageReceived - messageType: ${message.messageType}") + + val nickname = viewModel.getUserNickname(fromMember.userId!!.toInt()) + val profileUrl = viewModel.getUserProfileUrl(fromMember.userId!!.toInt()) + val rank = viewModel.getUserRank(fromMember.userId!!.toLong()) + + if (message.messageType == RtmMessageType.RAW) { + val rawMessage = Gson().fromJson( + String(message.rawMessage), + LiveRoomChatRawMessage::class.java + ) + + when (rawMessage.type) { + LiveRoomChatRawMessageType.EDIT_ROOM_INFO, + LiveRoomChatRawMessageType.SET_MANAGER -> { + handler.post { + viewModel.getRoomInfo(roomId) + } + } + + LiveRoomChatRawMessageType.DONATION -> { + handler.post { + chatAdapter.items.add( + LiveRoomDonationChat( + profileUrl, + nickname, + rawMessage.message, + rawMessage.can, + rawMessage.donationMessage ?: "" + ) + ) + invalidateChat() + viewModel.addDonationCan(rawMessage.can) + } + } + + LiveRoomChatRawMessageType.DONATION_STATUS -> { + handler.post { + chatAdapter.items.add( + LiveRoomDonationStatusChat( + donationStatusString = rawMessage.message + ) + ) + chatAdapter.notifyDataSetChanged() + invalidateChat() + } + } + } + } else { + val chat = message.text + + if (chat.isNotBlank()) { + handler.post { + chatAdapter.items.add( + LiveRoomNormalChat( + userId = fromMember.userId.toLong(), + profileUrl = profileUrl, + nickname = nickname, + rank = rank, + chat = chat + ) + ) + invalidateChat() + } + } + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onMemberJoined(member: RtmChannelMember) { + Logger.e("onMemberJoined: ${member.userId}") + viewModel.getRoomInfo(roomId, member.userId.toInt()) { + if (it.isNotBlank()) { + chatAdapter.items.add(LiveRoomJoinChat(it)) + invalidateChat() + } + } + } + + override fun onMemberLeft(member: RtmChannelMember) { + Logger.e("onMemberLeft: ${member.userId}") + if (!viewModel.isEqualToHostId(member.userId.toInt())) { + viewModel.getRoomInfo(roomId) + } + } + }, + rtmChannelJoinSuccess = { + if (userId == roomInfo.managerId) { + setBroadcaster() + } else { + setAudience() + } + + val intent = Intent(this, SodaLiveService::class.java) + intent.putExtra("roomId", roomId) + intent.putExtra("content", "라이브 진행중 - ${roomInfo.title}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + }, + rtmChannelJoinFail = { + agoraConnectFail() + } + ) + } + + private fun agoraConnectFail() { + showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.") + finish() + } + + private val rtcEventHandler = object : IRtcEngineEventHandler() { + @SuppressLint("NotifyDataSetChanged") + override fun onAudioVolumeIndication( + speakers: Array, + totalVolume: Int + ) { + super.onAudioVolumeIndication(speakers, totalVolume) + val activeSpeakerIds = speakers + .asSequence() + .filter { it.volume > 0 } + .map { it.uid } + .toList() + + Logger.e("onAudioVolumeIndication - $activeSpeakerIds") + handler.post { + speakerListAdapter.activeSpeakers.clear() + speakerListAdapter.activeSpeakers.addAll(activeSpeakerIds) + + if (activeSpeakerIds.contains(0) && !isMicrophoneMute) { + speakerListAdapter.activeSpeakers.add(SharedPreferenceManager.userId.toInt()) + } + speakerListAdapter.notifyDataSetChanged() + } + } + + override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) { + super.onJoinChannelSuccess(channel, uid, elapsed) + Logger.e("onJoinChannelSuccess - uid: $uid, channel: $channel") + } + + override fun onActiveSpeaker(uid: Int) { + Logger.e("onActiveSpeaker - uid: $uid") + super.onActiveSpeaker(uid) + } + + override fun onClientRoleChanged( + oldRole: Int, + newRole: Int, + newRoleOptions: ClientRoleOptions? + ) { + super.onClientRoleChanged(oldRole, newRole, newRoleOptions) + Logger.e( + "onClientRoleChanged - $oldRole - $newRole - " + + "${newRoleOptions?.audienceLatencyLevel}" + ) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onUserMuteAudio(uid: Int, muted: Boolean) { + super.onUserMuteAudio(uid, muted) + handler.post { + if (muted) { + speakerListAdapter.muteSpeakers.add(uid) + } else { + speakerListAdapter.muteSpeakers.remove(uid) + } + speakerListAdapter.notifyDataSetChanged() + } + Logger.e("onUserMuteAudio - uid: $uid, muted: $muted") + } + + override fun onUserJoined(uid: Int, elapsed: Int) { + super.onUserJoined(uid, elapsed) + Logger.e("onUserJoined - uid: $uid") + viewModel.getRoomInfo(roomId) + speakerListAdapter.muteSpeakers.remove(uid) + } + + override fun onUserOffline(uid: Int, reason: Int) { + super.onUserOffline(uid, reason) + Logger.e("onUserOffline - uid: $uid") + if (viewModel.isEqualToHostId(uid)) { + handler.post { + showToast("라이브가 종료되었습니다.") + finish() + } + } else { + viewModel.getRoomInfo(roomId) + speakerListAdapter.muteSpeakers.remove(uid) + } + } + } + + private val rtmClientListener = object : RtmClientListener { + override fun onConnectionStateChanged(state: Int, reason: Int) { + val text = + "Connection state changed to $state Reason: $reason".trimIndent() + + Logger.e(text) + } + + override fun onTokenExpired() {} + override fun onTokenPrivilegeWillExpire() {} + + override fun onPeersOnlineStatusChanged(map: Map?) {} + override fun onMessageReceived(rtmMessage: RtmMessage, peerId: String) { + Logger.e("text - ${rtmMessage.text}") + Logger.e("rawMessage - ${String(rtmMessage.rawMessage)}") + Logger.e("messageType - ${rtmMessage.messageType}") + if (rtmMessage.messageType == RtmMessageType.RAW) { + val rawMessage = String(rtmMessage.rawMessage) + + if (rawMessage == LiveRoomRequestType.CHANGE_LISTENER.toString()) { + handler.post { + viewModel.setListener( + roomId, + SharedPreferenceManager.userId + ) { + setAudience() + viewModel.getRoomInfo(roomId) + + if (roomUserProfileDialog.isShowing()) { + viewModel.getUserProfile( + roomId = roomId, + userId = peerId.toLong() + ) {} + } + } + } + + return + } + + if (rawMessage == LiveRoomRequestType.INVITE_SPEAKER.toString() && + !isSpeaker + ) { + handler.post { + showDialog( + content = "스피커로 초대되었어요", + cancelTitle = "다음에요", + cancelAction = {}, + confirmTitle = "스피커로 참여하기", + confirmAction = { + handler.post { + viewModel.setSpeaker( + roomId, + SharedPreferenceManager.userId + ) { + showDialog(content = "스피커가 되었어요!") + setBroadcaster() + viewModel.getRoomInfo(roomId) + } + } + } + ) + } + + return + } + + if (rawMessage == LiveRoomRequestType.KICK_OUT.toString()) { + handler.post { + finish() + showToast( + "${viewModel.getManagerNickname()}님이 라이브에서 내보냈습니다." + ) + } + + return + } + + if (rawMessage == LiveRoomRequestType.SET_MANAGER.toString()) { + if (isSpeaker) { + setAudience() + } + handler.post { + showDialog( + content = "${viewModel.getManagerNickname()}님이 스탭으로 지정했습니다." + ) + } + return + } + + if (rawMessage == LiveRoomRequestType.RELEASE_MANAGER.toString()) { + handler.post { + showDialog( + content = "${viewModel.getManagerNickname()}님이 스탭에서 해제했습니다." + ) + } + viewModel.getRoomInfo(roomId = roomId) + return + } + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt new file mode 100644 index 0000000..44b15f0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.live.room + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class LiveRoomDialog( + activity: Activity, + layoutInflater: LayoutInflater, +) { + + private val alertDialog: AlertDialog + private val dialogView = DialogLiveRoomBinding.inflate(layoutInflater) + private val handler = Handler(Looper.getMainLooper()) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + fun show(width: Int) { + if (!alertDialog.isShowing) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (53.4f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + + if (dialogView.llActionButtons.visibility == View.GONE) { + alertDialog.setCancelable(true) + val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams + llContentLp.bottomMargin = 0f.dpToPx().toInt() + dialogView.llContent.layoutParams = llContentLp + + handler.postDelayed({ + dismiss() + }, 1000) + } else { + alertDialog.setCancelable(false) + val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams + llContentLp.bottomMargin = 10f.dpToPx().toInt() + dialogView.llContent.layoutParams = llContentLp + } + } + } + + private fun dismiss() { + alertDialog.dismiss() + dialogView.tvContent.text = "" + dialogView.llActionButtons.visibility = View.GONE + dialogView.tvCancel.text = "" + dialogView.tvConfirm.text = "" + dialogView.tvCancel.setOnClickListener { } + dialogView.tvConfirm.setOnClickListener { } + + val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams + llContentLp.bottomMargin = 10f.dpToPx().toInt() + dialogView.llContent.layoutParams = llContentLp + } + + fun setCancel(title: String, action: () -> Unit) { + dialogView.tvCancel.text = title + dialogView.tvCancel.setOnClickListener { + dismiss() + action() + } + + dialogView.llActionButtons.visibility = View.VISIBLE + } + + fun setConfirm(title: String, action: () -> Unit) { + dialogView.tvConfirm.text = title + dialogView.tvConfirm.setOnClickListener { + dismiss() + action() + } + + dialogView.llActionButtons.visibility = View.VISIBLE + } + + fun setContent(content: String) { + dialogView.tvContent.text = content + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt new file mode 100644 index 0000000..baabfe6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.live.room + +import com.google.gson.annotations.SerializedName + +enum class LiveRoomRequestType { + @SerializedName("INVITE_SPEAKER") INVITE_SPEAKER, + @SerializedName("CHANGE_LISTENER") CHANGE_LISTENER, + @SerializedName("KICK_OUT") KICK_OUT, + @SerializedName("SET_MANAGER") SET_MANAGER, + @SerializedName("RELEASE_MANAGER") RELEASE_MANAGER, +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt new file mode 100644 index 0000000..acd4fdf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt @@ -0,0 +1,778 @@ +package kr.co.vividnext.sodalive.live.room + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.firebase.dynamiclinks.ShortDynamicLink +import com.google.firebase.dynamiclinks.ktx.androidParameters +import com.google.firebase.dynamiclinks.ktx.dynamicLinks +import com.google.firebase.dynamiclinks.ktx.iosParameters +import com.google.firebase.dynamiclinks.ktx.shortLinkAsync +import com.google.firebase.ktx.Firebase +import com.google.gson.Gson +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.live.LiveRepository +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse +import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse +import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse +import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest +import kr.co.vividnext.sodalive.report.ReportRepository +import kr.co.vividnext.sodalive.report.ReportRequest +import kr.co.vividnext.sodalive.report.ReportType +import kr.co.vividnext.sodalive.user.UserRepository +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File + +class LiveRoomViewModel( + private val repository: LiveRepository, + private val userRepository: UserRepository, + private val reportRepository: ReportRepository +) : BaseViewModel() { + private val _roomInfoLiveData = MutableLiveData() + val roomInfoLiveData: LiveData + get() = _roomInfoLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private val _isShowNotice = MutableLiveData(true) + val isShowNotice: LiveData + get() = _isShowNotice + + private val _isExpandNotice = MutableLiveData(false) + val isExpandNotice: LiveData + get() = _isExpandNotice + + private val _totalDonationCan = MutableLiveData(0) + val totalDonationCan: LiveData + get() = _totalDonationCan + + private val _userProfileLiveData = MutableLiveData() + val userProfileLiveData: LiveData + get() = _userProfileLiveData + + lateinit var roomInfoResponse: GetRoomInfoResponse + + fun isRoomInfoInitialized() = this::roomInfoResponse.isInitialized + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _isBgOn = MutableLiveData(true) + val isBgOn: LiveData + get() = _isBgOn + + lateinit var getRealPathFromURI: (Uri) -> String? + + fun getUserNickname(memberId: Int): String { + for (manager in roomInfoResponse.managerList) { + if (manager.id.toInt() == memberId) { + return manager.nickname + } + } + + for (speaker in roomInfoResponse.speakerList) { + if (speaker.id.toInt() == memberId) { + return speaker.nickname + } + } + + for (listener in roomInfoResponse.listenerList) { + if (listener.id.toInt() == memberId) { + return listener.nickname + } + } + + return "" + } + + fun getUserProfileUrl(accountId: Int): String { + for (manager in roomInfoResponse.managerList) { + if (manager.id.toInt() == accountId) { + return manager.profileImage + } + } + + for (speaker in roomInfoResponse.speakerList) { + if (speaker.id.toInt() == accountId) { + return speaker.profileImage + } + } + + for (listener in roomInfoResponse.listenerList) { + if (listener.id.toInt() == accountId) { + return listener.profileImage + } + } + + return "" + } + + fun getManagerNickname(): String { + return roomInfoResponse.managerNickname + } + + fun setSpeaker(roomId: Long, userId: Long, onSuccess: () -> Unit) { + compositeDisposable.add( + repository.setSpeaker(roomId, userId, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun setListener(roomId: Long, userId: Long, onSuccess: () -> Unit) { + compositeDisposable.add( + repository.setListener(roomId, userId, "Bearer ${SharedPreferenceManager.token}") + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun getRoomInfo(roomId: Long, userId: Int = 0, onSuccess: (String) -> Unit = {}) { + compositeDisposable.add( + repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + roomInfoResponse = it.data + Logger.e("data: ${it.data}") + _roomInfoLiveData.postValue(roomInfoResponse) + + getTotalDonationCan(roomId = roomId) + + if (userId > 0) { + val nickname = getUserNickname(userId) + onSuccess(nickname) + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun isEqualToHostId(accountId: Int): Boolean { + return accountId == roomInfoResponse.managerId.toInt() + } + + fun getMemberCan() { + compositeDisposable.add( + userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + SharedPreferenceManager.can = it.data.can + } + }, + { + } + ) + ) + } + + fun shareRoomLink( + roomId: Long, + isPrivateRoom: Boolean, + password: String?, + onSuccess: (String) -> Unit + ) { + _isLoading.value = true + Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { + link = Uri.parse("https://yozm.day/?room_id=$roomId") + domainUriPrefix = "https://yozm.page.link" + androidParameters { } + iosParameters("kr.co.vividnext.yozm") { + appStoreId = "1630284226" + } + }.addOnSuccessListener { + val uri = it.shortLink + if (uri != null) { + val message = if (isPrivateRoom) { + "${SharedPreferenceManager.nickname}님이 귀하를 " + + "소다라이브의 비공개라이브에 초대하였습니다.\n" + + "※ 라이브 참여: $uri\n" + + "(입장 비밀번호 : $password)" + } else { + "${SharedPreferenceManager.nickname}님이 귀하를 " + + "소다라이브의 공개라이브에 초대하였습니다.\n" + + "※ 라이브 참여: $uri" + } + + onSuccess(message) + } + }.addOnFailureListener { + _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.") + }.addOnCompleteListener { + _isLoading.value = false + } + } + + fun creatorFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) { + _isLoading.value = true + compositeDisposable.add( + repository.creatorFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getRoomInfo(roomId) + + if (isGetUserProfile) { + getUserProfile(roomId = roomId, userId = creatorId) {} + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun creatorUnFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) { + _isLoading.value = true + compositeDisposable.add( + repository.creatorUnFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getRoomInfo(roomId) + + if (isGetUserProfile) { + getUserProfile(roomId = roomId, userId = creatorId) {} + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun toggleShowNotice() { + _isShowNotice.value = !isShowNotice.value!! + _isExpandNotice.value = false + } + + fun toggleExpandNotice() { + _isExpandNotice.value = !isExpandNotice.value!! + } + + fun toggleBackgroundImage() { + _isBgOn.value = !isBgOn.value!! + } + + fun editLiveRoomInfo( + roomId: Long, + newTitle: String, + newContent: String, + newCoverImageUri: Uri? = null, + onSuccess: () -> Unit + ) { + val request = EditLiveRoomInfoRequest( + title = if (newTitle != roomInfoResponse.title) { + newTitle + } else { + null + }, + notice = if (newContent != roomInfoResponse.notice) { + newContent + } else { + null + }, + numberOfPeople = null, + beginDateTimeString = null, + timezone = null + ) + + val requestJson = if (request.title != null || request.notice != null) { + Gson().toJson(request) + } else { + null + } + + val coverImage = if (newCoverImageUri != null) { + val file = File(getRealPathFromURI(newCoverImageUri!!)) + MultipartBody.Part.createFormData( + "coverImage", + file.name, + file.asRequestBody("image/*".toMediaType()) + ) + } else { + null + } + + if (coverImage == null && requestJson == null) { + _toastLiveData.value = "변경사항이 없습니다." + return + } + + compositeDisposable.add( + repository.editLiveRoomInfo( + roomId = roomId, + coverImage = coverImage, + request = requestJson?.toRequestBody("text/plain".toMediaType()), + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + getRoomInfo(roomId = roomId) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." + ) + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요." + ) + } + ) + ) + } + + fun quitRoom(roomId: Long, onSuccess: () -> Unit) { + _isLoading.value = true + compositeDisposable.add( + repository.quitRoom(roomId = roomId, token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success) { + onSuccess() + _isLoading.value = false + } else { + _isLoading.value = false + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun kickOut(roomId: Long, userId: Long) { + compositeDisposable.add( + repository.kickOut(roomId, userId, token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({}, {}) + ) + } + + fun donation(roomId: Long, can: Int, message: String, onSuccess: () -> Unit) { + _isLoading.postValue(true) + compositeDisposable.add( + repository.donation(roomId, can, message, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + SharedPreferenceManager.can -= can + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun refundDonation(roomId: Long) { + _isLoading.postValue(true) + + compositeDisposable.add( + repository.refundDonation(roomId, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + _toastLiveData.postValue( + "후원에 실패했습니다.\n다시 후원해주세요.\n" + + "계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + ) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요." + ) + } + ) + ) + } + + fun addDonationCan(can: Int) { + _totalDonationCan.postValue(totalDonationCan.value!! + can) + } + + fun donationStatus(roomId: Long, onSuccess: (GetLiveRoomDonationStatusResponse) -> Unit) { + _isLoading.value = true + compositeDisposable.add( + repository.donationStatus(roomId, token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + onSuccess(it.data) + } else { + _toastLiveData.postValue( + "후원현황을 가져오지 못했습니다\n다시 시도해 주세요." + ) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "후원현황을 가져오지 못했습니다\n다시 시도해 주세요." + ) + } + ) + ) + } + + fun getUserRank(userId: Long): Int { + // 방장 -> -2 + // 스탭 -> -3 + // 나머지 -> 체크 + return if (isEqualToHostId(userId.toInt())) { + -2 + } else if (isEqualToManagerId(userId.toInt())) { + -3 + } else { + roomInfoResponse.donationRankingTop3UserIds.indexOf(userId) + } + } + + private fun getTotalDonationCan(roomId: Long) { + compositeDisposable.add( + repository.getTotalDonationCan( + roomId = roomId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _totalDonationCan.postValue(it.data.totalDonationCan) + } + }, + { + } + ) + ) + } + + fun getUserProfile(roomId: Long, userId: Long, onSuccess: () -> Unit) { + _isLoading.value = true + compositeDisposable.add( + repository.getUserProfile( + roomId = roomId, + userId = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + _userProfileLiveData.value = it.data!! + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun setManager(roomId: Long, userId: Long, onSuccess: () -> Unit) { + _isLoading.value = true + compositeDisposable.add( + repository.setManager(roomId, userId, "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + getRoomInfo(roomId) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun isEqualToManagerId(accountId: Int): Boolean { + for (manager in roomInfoResponse.managerList) { + if (manager.id == accountId.toLong()) { + return true + } + } + + return false + } + + fun memberBlock(userId: Long, onSuccess: () -> Unit) { + _isLoading.value = true + compositeDisposable.add( + userRepository.memberBlock( + userId = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + _toastLiveData.postValue("차단하였습니다.") + onSuccess() + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun memberUnBlock(userId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.memberUnBlock( + userId = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + getUserProfile(roomId = roomInfoResponse.roomId, userId = userId) {} + _toastLiveData.postValue("차단이 해제 되었습니다.") + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun report(type: ReportType, userId: Long, reason: String = "프로필 신고") { + _isLoading.value = true + + val request = ReportRequest(type, reason, reportedAccountId = userId) + compositeDisposable.add( + reportRepository.report( + request = request, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "신고가 접수되었습니다." + ) + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("신고가 접수되었습니다.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt new file mode 100644 index 0000000..b701b92 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room + +import com.google.gson.annotations.SerializedName + +data class SetManagerOrSpeakerOrAudienceRequest( + @SerializedName("roomId") val roomId: Long, + @SerializedName("accountId") val accountId: Long +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt new file mode 100644 index 0000000..32c801d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt @@ -0,0 +1,336 @@ +package kr.co.vividnext.sodalive.live.room.chat + +import android.annotation.SuppressLint +import android.content.Context +import android.text.SpannableString +import android.text.Spanned +import android.text.TextUtils +import android.text.style.ForegroundColorSpan +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.setPadding +import androidx.viewbinding.ViewBinding +import coil.load +import coil.transform.RoundedCornersTransformation +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.CustomTypefaceSpan +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomChatBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationStatusChatBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomJoinChatBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse + +enum class LiveRoomChatType { + @SerializedName("CHAT") + CHAT, + + @SerializedName("DONATION_STATUS") + DONATION_STATUS, + + @SerializedName("JOIN") + JOIN +} + +abstract class LiveRoomChat { + open var type: LiveRoomChatType = LiveRoomChatType.CHAT + abstract fun bind( + context: Context, + binding: ViewBinding, + onClickProfile: ((Long) -> Unit)? = null + ) +} + +data class LiveRoomJoinChat( + val nickname: String +) : LiveRoomChat() { + override var type = LiveRoomChatType.JOIN + override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) { + val str = "'$nickname'님이 입장하셨습니다." + val spStr = SpannableString(str) + + spStr.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + context, + R.color.color_ffdc00 + ) + ), + str.indexOf("'") + 1, + str.indexOf("'님"), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + spStr.setSpan( + CustomTypefaceSpan( + ResourcesCompat.getFont( + context, + R.font.gmarket_sans_bold + ) + ), + str.indexOf("'"), + str.indexOf("'님"), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + (binding as ItemLiveRoomJoinChatBinding).tvJoin.text = spStr + } +} + +data class LiveRoomDonationStatusChat( + val response: GetLiveRoomDonationStatusResponse? = null, + val donationStatusString: String? = null +) : LiveRoomChat() { + override var type = LiveRoomChatType.DONATION_STATUS + override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) { + if (donationStatusString != null) { + (binding as ItemLiveRoomDonationStatusChatBinding) + .tvDonationList + .text = donationStatusString + } else { + (binding as ItemLiveRoomDonationStatusChatBinding) + .tvDonationList + .text = getDonationString(context) + } + } + + fun getDonationString(context: Context): String { + if (response != null) { + var donationStatusString: CharSequence = "[현재 라이브 후원현황]\n\n" + for (index in response.donationList.indices) { + val donation = response.donationList[index] + val spChars = SpannableString( + "${index + 1}. " + + if (donation.nickname.length > 10) { + "${donation.nickname.substring(0, 10)} : " + } else { + "${donation.nickname} : " + } + "(${donation.can.moneyFormat()} 캔)\n" + ) + spChars.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + context, + R.color.color_fdca2f + ) + ), + spChars.indexOf(": (") + 3, + spChars.indexOf(")"), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + donationStatusString = TextUtils.concat( + donationStatusString, spChars + ) + } + + donationStatusString = TextUtils.concat( + donationStatusString, + "\n-------------------------\n\n" + ) + + donationStatusString = TextUtils.concat( + donationStatusString, + "후원인원 : ${response.totalCount} 명\n" + ) + + donationStatusString = TextUtils.concat( + donationStatusString, + "후원합계 : ${response.totalCan.moneyFormat()} 캔" + ) + + return donationStatusString.toString() + } else { + return "" + } + } +} + +data class LiveRoomNormalChat( + @SerializedName("userId") val userId: Long, + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("rank") val rank: Int, + @SerializedName("chat") val chat: String, +) : LiveRoomChat() { + override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) { + val itemBinding = binding as ItemLiveRoomChatBinding + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(23.3f.dpToPx())) + } + + itemBinding.ivProfile.setOnClickListener { + if (onClickProfile != null && userId != SharedPreferenceManager.userId) { + onClickProfile(userId) + } + } + + itemBinding.tvChat.text = chat + itemBinding.tvNickname.text = nickname + + itemBinding.ivBg.visibility = View.VISIBLE + itemBinding.tvCreatorOrManager.visibility = View.GONE + + when (rank + 1) { + -2 -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_4999e3) + itemBinding.ivCrown.setImageResource(R.drawable.ic_badge_manager) + itemBinding.tvCreatorOrManager.setBackgroundResource( + R.drawable.bg_round_corner_2_4999e3 + ) + itemBinding.tvCreatorOrManager.text = "스탭" + + itemBinding.ivCrown.visibility = View.VISIBLE + itemBinding.tvCreatorOrManager.visibility = View.VISIBLE + } + + -1 -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_6f3dec_9970ff) + itemBinding.ivCrown.setImageResource(R.drawable.ic_crown) + itemBinding.ivCrown.visibility = View.VISIBLE + } + + 1 -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_fdca2f) + itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_1) + itemBinding.ivCrown.visibility = View.VISIBLE + } + + 2 -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_9f9f9f_dcdcdc) + itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_2) + itemBinding.ivCrown.visibility = View.VISIBLE + } + + 3 -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_e5a578_c67e4a) + itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_3) + itemBinding.ivCrown.visibility = View.VISIBLE + } + + else -> { + itemBinding.ivBg.setImageResource(R.drawable.bg_circle_9f9f9f_bbbbbb) + itemBinding.ivCrown.visibility = View.GONE + } + } + + val messageHorizontalPadding = 8.dpToPx().toInt() + val messageVerticalPadding = 5.3f.dpToPx().toInt() + itemBinding.llMessageBg.setPadding( + messageHorizontalPadding, + messageVerticalPadding, + messageHorizontalPadding, + messageVerticalPadding + ) + + if (SharedPreferenceManager.userId == userId) { + itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_999970ff) + } else { + itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_99000000) + } + itemBinding.ivCoin.visibility = View.GONE + itemBinding.tvDonationMessage.visibility = View.GONE + itemBinding.root.setBackgroundResource(0) + itemBinding.root.setPadding(0) + } +} + +data class LiveRoomDonationChat( + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("chat") val chat: String, + @SerializedName("coin") val coin: Int, + @SerializedName("donationMessage") val donationMessage: String, +) : LiveRoomChat() { + @SuppressLint("SetTextI18n") + override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) { + val itemBinding = binding as ItemLiveRoomChatBinding + val spChat = SpannableString(chat) + spChat.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + context, + R.color.color_fdca2f + ) + ), + 0, + chat.indexOf("코인", 0, true) + 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + val spNickname = SpannableString("${nickname}님이") + spNickname.setSpan( + CustomTypefaceSpan( + ResourcesCompat.getFont( + context, + R.font.gmarket_sans_medium + ) + ), + 0, + nickname.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(23.3f.dpToPx())) + } + itemBinding.tvChat.text = spChat + itemBinding.tvNickname.text = spNickname + itemBinding.ivProfile.setOnClickListener {} + + itemBinding.ivCoin.visibility = View.VISIBLE + itemBinding.ivBg.visibility = View.GONE + itemBinding.ivCrown.visibility = View.GONE + itemBinding.tvCreatorOrManager.visibility = View.GONE + + if (donationMessage.isNotBlank()) { + itemBinding.tvDonationMessage.text = "\"$donationMessage\"" + itemBinding.tvDonationMessage.visibility = View.VISIBLE + } else { + itemBinding.tvDonationMessage.visibility = View.GONE + } + + itemBinding.llMessageBg.setPadding(0) + itemBinding.llMessageBg.background = null + + itemBinding.root.setBackgroundResource( + when { + coin >= 100000 -> { + R.drawable.bg_round_corner_6_7_c25264 + } + + coin >= 50000 -> { + R.drawable.bg_round_corner_6_7_e6d85e37 + } + + coin >= 10000 -> { + R.drawable.bg_round_corner_6_7_e6d38c38 + } + + coin >= 5000 -> { + R.drawable.bg_round_corner_6_7_e659548f + } + + coin >= 1000 -> { + R.drawable.bg_round_corner_6_7_e64d6aa4 + } + + coin >= 500 -> { + R.drawable.bg_round_corner_6_7_e62d7390 + } + + else -> { + R.drawable.bg_round_corner_6_7_e6548f7d + } + } + ) + itemBinding.root.setPadding(33) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt new file mode 100644 index 0000000..020952a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.live.room.chat + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomChatBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationStatusChatBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomJoinChatBinding + +class LiveRoomChatAdapter( + private val onClickProfile: (Long) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveRoomChatViewHolder { + when (viewType) { + LiveRoomChatType.DONATION_STATUS.ordinal -> { + return LiveRoomDonationStatusChatViewHolder( + parent.context, + ItemLiveRoomDonationStatusChatBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + LiveRoomChatType.JOIN.ordinal -> { + return LiveRoomJoinChatViewHolder( + parent.context, + ItemLiveRoomJoinChatBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + else -> { + return LiveRoomNormalChatViewHolder( + parent.context, + ItemLiveRoomChatBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + } + } + + override fun onBindViewHolder(holder: LiveRoomChatViewHolder, position: Int) { + holder.bind(items[position], onClickProfile) + } + + override fun getItemCount() = items.count() + + override fun getItemViewType(position: Int): Int { + return items[position].type.ordinal + } +} + +abstract class LiveRoomChatViewHolder(binding: ViewBinding) : + RecyclerView.ViewHolder(binding.root) { + abstract fun bind(chat: LiveRoomChat, onClickProfile: ((Long) -> Unit)? = null) +} + +class LiveRoomNormalChatViewHolder( + private val context: Context, + private val binding: ItemLiveRoomChatBinding +) : LiveRoomChatViewHolder(binding) { + override fun bind( + chat: LiveRoomChat, + onClickProfile: ((Long) -> Unit)? + ) = chat.bind(context, binding, onClickProfile) +} + +class LiveRoomDonationStatusChatViewHolder( + private val context: Context, + private val binding: ItemLiveRoomDonationStatusChatBinding +) : LiveRoomChatViewHolder(binding) { + override fun bind( + chat: LiveRoomChat, + onClickProfile: ((Long) -> Unit)? + ) = chat.bind(context, binding) +} + +class LiveRoomJoinChatViewHolder( + private val context: Context, + private val binding: ItemLiveRoomJoinChatBinding +) : LiveRoomChatViewHolder(binding) { + override fun bind( + chat: LiveRoomChat, + onClickProfile: ((Long) -> Unit)? + ) = chat.bind(context, binding) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt new file mode 100644 index 0000000..26c32ac --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.live.room.chat + +import com.google.gson.annotations.SerializedName + +data class LiveRoomChatRawMessage( + @SerializedName("type") val type: LiveRoomChatRawMessageType, + @SerializedName("message") val message: String, + @SerializedName("can") val can: Int, + @SerializedName("donationMessage") val donationMessage: String? +) + +enum class LiveRoomChatRawMessageType { + @SerializedName("DONATION") DONATION, + @SerializedName("SET_MANAGER") SET_MANAGER, + @SerializedName("EDIT_ROOM_INFO") EDIT_ROOM_INFO, + @SerializedName("DONATION_STATUS") DONATION_STATUS +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt new file mode 100644 index 0000000..717c063 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.google.gson.annotations.SerializedName + +data class DeleteLiveRoomDonationMessage( + @SerializedName("roomId") val roomId: Long, + @SerializedName("messageUUID") val messageUUID: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt new file mode 100644 index 0000000..138859a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.google.gson.annotations.SerializedName + +data class GetLiveRoomDonationStatusResponse( + @SerializedName("donationList") val donationList: List, + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("totalCan") val totalCan: Int +) + +data class GetLiveRoomDonationItem( + @SerializedName("profileImage") val profileImage: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("userId") val userId: Long, + @SerializedName("can") val can: Int +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt new file mode 100644 index 0000000..8e9bd14 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.google.gson.annotations.SerializedName + +data class GetLiveRoomDonationTotalResponse( + @SerializedName("totalDonationCan") val totalDonationCan: Int +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt new file mode 100644 index 0000000..4a9b464 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt @@ -0,0 +1,108 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import coil.load +import coil.transform.CircleCropTransformation +import com.google.android.material.bottomsheet.BottomSheetDialog +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationBinding +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity + +class LiveRoomDonationDialog( + private val activity: AppCompatActivity, + layoutInflater: LayoutInflater, + onClickDonation: (Int, String) -> Unit +) { + + private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity) + private val dialogView = DialogLiveRoomDonationBinding.inflate(layoutInflater) + + init { + bottomSheetDialog.setContentView(dialogView.root) + bottomSheetDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + bottomSheetDialog.setCancelable(false) + + dialogView.tvCancel.setOnClickListener { bottomSheetDialog.dismiss() } + dialogView.tvDonation.setOnClickListener { + try { + val coin = dialogView.etDonationCoin.text.toString().toInt() + val message = dialogView.etDonationMessage.text.toString() + + if (coin > 0) { + bottomSheetDialog.dismiss() + onClickDonation(coin, message) + } else { + Toast.makeText( + activity, + "1코인 이상 후원하실 수 있습니다.", + Toast.LENGTH_LONG + ).show() + } + } catch (e: NumberFormatException) { + Toast.makeText( + activity, + "1코인 이상 후원하실 수 있습니다.", + Toast.LENGTH_LONG + ).show() + } + } + + setupView() + } + + fun show(width: Int) { + if (!bottomSheetDialog.isShowing) { + bottomSheetDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(bottomSheetDialog.window?.attributes) + lp.width = width + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + bottomSheetDialog.window?.attributes = lp + } + } + + @SuppressLint("SetTextI18n") + private fun setupView() { + dialogView.tvCoin.text = SharedPreferenceManager.can.moneyFormat() + dialogView.tvPlus10.setOnClickListener { addCoin(10) } + dialogView.tvPlus100.setOnClickListener { addCoin(100) } + dialogView.tvPlus1000.setOnClickListener { addCoin(1000) } + dialogView.tvPlus10000.setOnClickListener { addCoin(10000) } + + dialogView.ivProfile.load(SharedPreferenceManager.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + dialogView.tvCoin.setOnClickListener { + bottomSheetDialog.dismiss() + + val intent = Intent(activity, CanChargeActivity::class.java) + intent.putExtra(Constants.EXTRA_PREV_LIVE_ROOM, true) + activity.startActivity(intent) + } + } + + @SuppressLint("SetTextI18n") + private fun addCoin(coin: Int) { + try { + val currentCoin = dialogView.etDonationCoin.text.toString().toInt() + dialogView.etDonationCoin.setText((currentCoin + coin).toString()) + } catch (e: NumberFormatException) { + dialogView.etDonationCoin.setText(coin.toString()) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt new file mode 100644 index 0000000..f5c9fb5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.google.gson.annotations.SerializedName + +data class LiveRoomDonationMessage( + @SerializedName("uuid") val uuid: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("canMessage") val canMessage: String, + @SerializedName("donationMessage") val donationMessage: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt new file mode 100644 index 0000000..17c1338 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt @@ -0,0 +1,62 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationMessageBinding + +class LiveRoomDonationMessageAdapter( + private val onClickDeleteMessage: (String) -> Unit, + private val copyMessage: (String) -> Unit +) : RecyclerView.Adapter() { + + private var items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemLiveRoomDonationMessageBinding + ) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(item: LiveRoomDonationMessage) { + binding.tvNickname.text = "${item.nickname}님이" + binding.tvCoinMessage.text = item.canMessage + binding.tvDonationMessage.text = "\"${item.donationMessage}\"" + binding.ivDelete.setOnClickListener { onClickDeleteMessage(item.uuid) } + + binding.root.setOnClickListener { copyMessage(item.donationMessage) } + } + } + + fun update(items: List) { + items.let { + val diffCallback = LiveRoomDonationMessageDiffUtilCallback(this.items, items) + val diffResult = DiffUtil.calculateDiff(diffCallback) + + this.items.run { + clear() + addAll(it) + diffResult.dispatchUpdatesTo(this@LiveRoomDonationMessageAdapter) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemLiveRoomDonationMessageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int { + return items.size + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt new file mode 100644 index 0000000..3041a7f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt @@ -0,0 +1,115 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import android.annotation.SuppressLint +import android.graphics.Color +import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.view.Window +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.LiveData +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationMessageBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class LiveRoomDonationMessageDialog( + layoutInflater: LayoutInflater, + private val activity: FragmentActivity, + private val donationMessageListLiveData: LiveData>, + private val donationMessageCountLiveData: LiveData, + private val getDonationMessageList: () -> Unit, + private val deleteDonationMessage: (String) -> Unit, + copyMessage: (String) -> Unit +) { + private val alertDialog: AlertDialog + private val dialogView = DialogLiveRoomDonationMessageBinding.inflate(layoutInflater) + private val adapter = LiveRoomDonationMessageAdapter( + onClickDeleteMessage = { deleteDonationMessage(it) }, + copyMessage = copyMessage + ) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + alertDialog.setOnShowListener { + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = activity.resources.displayMetrics.widthPixels - (26.7f.dpToPx()).toInt() + lp.height = activity.resources.displayMetrics.heightPixels - (200f.dpToPx()).toInt() + + alertDialog.window?.attributes = lp + } + + dialogView.tvClose.setOnClickListener { alertDialog.dismiss() } + setupRecyclerView() + bindData() + } + + private fun setupRecyclerView() { + dialogView.rvDonationMessage.isNestedScrollingEnabled = true + dialogView.rvDonationMessage.layoutManager = LinearLayoutManager(activity) + dialogView.rvDonationMessage.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 0.dpToPx().toInt() + outRect.bottom = 4.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 4.dpToPx().toInt() + outRect.bottom = 0.dpToPx().toInt() + } + + else -> { + outRect.top = 4.dpToPx().toInt() + outRect.bottom = 4.dpToPx().toInt() + } + } + } + }) + dialogView.rvDonationMessage.adapter = adapter + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + donationMessageListLiveData.observe(activity) { + if (it.isEmpty()) { + dialogView.rvDonationMessage.visibility = View.GONE + dialogView.tvNone.visibility = View.VISIBLE + } else { + dialogView.rvDonationMessage.visibility = View.VISIBLE + dialogView.tvNone.visibility = View.GONE + } + + adapter.update(it) + } + + donationMessageCountLiveData.observe(activity) { + dialogView.tvDonationMessageCount.text = "(${it.moneyFormat()})" + } + } + + fun show() { + alertDialog.show() + getDonationMessageList() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt new file mode 100644 index 0000000..ff9e118 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import androidx.recyclerview.widget.DiffUtil + +class LiveRoomDonationMessageDiffUtilCallback( + private val oldData: List, + private val newData: List +) : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldData[oldItemPosition] + val newItem = newData[newItemPosition] + + return oldItem.uuid == newItem.uuid + } + + override fun getOldListSize(): Int = oldData.size + + override fun getNewListSize(): Int = newData.size + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ) = oldData[oldItemPosition] == newData[newItemPosition] +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt new file mode 100644 index 0000000..ca07c57 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt @@ -0,0 +1,96 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.live.LiveRepository + +class LiveRoomDonationMessageViewModel(private val repository: LiveRepository) : BaseViewModel() { + private val _donationMessageListLiveData = MutableLiveData>() + val donationMessageListLiveData: LiveData> + get() = _donationMessageListLiveData + + private val _donationMessageCountLiveData = MutableLiveData(0) + val donationMessageCountLiveData: LiveData + get() = _donationMessageCountLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + fun getDonationMessageList(roomId: Long) { + _isLoading.value = true + compositeDisposable.add( + repository.getDonationMessageList( + roomId = roomId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + _donationMessageListLiveData.postValue(it.data!!) + _donationMessageCountLiveData.postValue(it.data!!.size) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun deleteDonationMessage(roomId: Long, uuid: String) { + _isLoading.value = true + compositeDisposable.add( + repository.deleteDonationMessage( + roomId = roomId, + uuid = uuid, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + getDonationMessageList(roomId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt new file mode 100644 index 0000000..26fe0ff --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt @@ -0,0 +1,146 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationRankingBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat + +class LiveRoomDonationRankingAdapter : + RecyclerView.Adapter() { + val items = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + parent.context, + ItemLiveRoomDonationRankingBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position], position) + } + + override fun getItemCount() = items.count() + + inner class ViewHolder( + private val context: Context, + private val binding: ItemLiveRoomDonationRankingBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") + fun bind(item: GetLiveRoomDonationItem, position: Int) { + binding.tvRank.text = "${position + 1}" + binding.tvNickname.text = item.nickname + + binding.ivProfile.load(item.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.tvDonationCan.text = item.can.moneyFormat() + + val lp = binding.rlDonationRanking.layoutParams as FrameLayout.LayoutParams + + when (position) { + 0 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_1) + binding.ivCrown.visibility = View.VISIBLE + binding.root.setBackgroundResource( + if (items.size == 1) { + R.drawable.bg_round_corner_4_7_2b2635 + } else { + R.drawable.bg_top_round_corner_4_7_2b2635 + } + ) + + lp.setMargins( + 0, + 20.dpToPx().toInt(), + 0, + if (items.size == 1) { + 20.dpToPx().toInt() + } else { + 13.3f.dpToPx().toInt() + } + ) + binding.rlDonationRanking.layoutParams = lp + } + + 1 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_2) + binding.ivCrown.visibility = View.VISIBLE + + if (items.size == 2) { + binding.root.setBackgroundResource( + R.drawable.bg_bottom_round_corner_4_7_2b2635 + ) + } else { + binding.root.setBackgroundColor( + ContextCompat.getColor(context, R.color.color_2b2635) + ) + } + + lp.setMargins( + 0, + 0, + 0, + if (items.size == 2) { + 20.dpToPx().toInt() + } else { + 13.3f.dpToPx().toInt() + } + ) + binding.rlDonationRanking.layoutParams = lp + } + + 2 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_3) + binding.ivCrown.visibility = View.VISIBLE + binding.root.setBackgroundResource( + R.drawable.bg_bottom_round_corner_4_7_2b2635 + ) + + lp.setMargins( + 0, + 0, + 0, + 20.dpToPx().toInt() + ) + binding.rlDonationRanking.layoutParams = lp + } + + else -> { + binding.ivBg.setImageResource(0) + binding.ivBg.visibility = View.GONE + binding.ivCrown.visibility = View.GONE + binding.root.setBackgroundResource(0) + binding.root.background = null + + lp.setMargins(0, 0, 0, 0) + binding.rlDonationRanking.layoutParams = lp + } + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt new file mode 100644 index 0000000..953581e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt @@ -0,0 +1,87 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import android.annotation.SuppressLint +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationRankingBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel + +@SuppressLint("NotifyDataSetChanged") +class LiveRoomDonationRankingDialog( + private val activity: FragmentActivity, + layoutInflater: LayoutInflater, + viewModel: LiveRoomViewModel, + roomId: Long +) { + private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity) + private val dialogView = DialogLiveRoomDonationRankingBinding.inflate(layoutInflater) + private val adapter = LiveRoomDonationRankingAdapter() + + init { + bottomSheetDialog.setContentView(dialogView.root) + bottomSheetDialog.setCancelable(false) + + dialogView.ivClose.setOnClickListener { bottomSheetDialog.dismiss() } + setupRecyclerView() + + viewModel.donationStatus(roomId) { + adapter.items.clear() + adapter.items.addAll(it.donationList) + adapter.notifyDataSetChanged() + dialogView.tvTotalCoin.text = it.totalCan.moneyFormat() + dialogView.tvTotalCount.text = it.totalCount.moneyFormat() + } + } + + private fun setupRecyclerView() { + dialogView.rvDonationRanking.layoutManager = LinearLayoutManager( + activity, + LinearLayoutManager.VERTICAL, + false + ) + + dialogView.rvDonationRanking.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + + when (parent.getChildAdapterPosition(view)) { + 0, 1, 2 -> { + outRect.top = 0 + outRect.bottom = 0 + } + + 3 -> { + outRect.top = 20.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + + else -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + } + } + }) + + dialogView.rvDonationRanking.adapter = adapter + } + + fun show() { + bottomSheetDialog.show() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt new file mode 100644 index 0000000..3f58f20 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.google.gson.annotations.SerializedName + +data class LiveRoomDonationRequest( + @SerializedName("roomId") val roomId: Long, + @SerializedName("can") val can: Int, + @SerializedName("message") val message: String, + @SerializedName("container") val container: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt new file mode 100644 index 0000000..ea5c2cf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.live.room.info + +import com.google.gson.annotations.SerializedName + +data class GetRoomInfoResponse( + @SerializedName("roomId") val roomId: Long, + @SerializedName("title") val title: String, + @SerializedName("notice") val notice: String, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("channelName") val channelName: String, + @SerializedName("rtcToken") val rtcToken: String, + @SerializedName("rtmToken") val rtmToken: String, + @SerializedName("managerId") val managerId: Long, + @SerializedName("managerNickname") val managerNickname: String, + @SerializedName("managerProfileUrl") val managerProfileUrl: String, + @SerializedName("isFollowingManager") val isFollowingManager: Boolean, + @SerializedName("participantsCount") val participantsCount: Int, + @SerializedName("totalAvailableParticipantsCount") val totalAvailableParticipantsCount: Int, + @SerializedName("speakerList") val speakerList: List, + @SerializedName("listenerList") val listenerList: List, + @SerializedName("managerList") val managerList: List, + @SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List, + @SerializedName("isAvailableDonation") val isAvailableDonation: Boolean = false, + @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean, + @SerializedName("password") val password: String? = null +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt new file mode 100644 index 0000000..e04614c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.live.room.info + +import com.google.gson.annotations.SerializedName + +data class LiveRoomMember( + @SerializedName("id") val id: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImage") val profileImage: String, + @SerializedName("role") val role: LiveRoomMemberRole +) + +enum class LiveRoomMemberRole { + @SerializedName("LISTENER") + LISTENER, + @SerializedName("SPEAKER") + SPEAKER, + @SerializedName("MANAGER") + MANAGER +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt new file mode 100644 index 0000000..f230d46 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room.kick_out + +import com.google.gson.annotations.SerializedName + +data class LiveRoomKickOutRequest( + @SerializedName("roomId") val roomId: Long, + @SerializedName("userId") val userId: Long +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt new file mode 100644 index 0000000..3aee820 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import com.google.gson.annotations.SerializedName + +data class GetLiveRoomUserProfileResponse( + @SerializedName("userId") val userId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("gender") val gender: String, + @SerializedName("instagramUrl") val instagramUrl: String, + @SerializedName("youtubeUrl") val youtubeUrl: String, + @SerializedName("websiteUrl") val websiteUrl: String, + @SerializedName("blogUrl") val blogUrl: String, + @SerializedName("introduce") val introduce: String, + @SerializedName("tags") val tags: String, + @SerializedName("isSpeaker") val isSpeaker: Boolean?, + @SerializedName("isManager") val isManager: Boolean?, + @SerializedName("isFollowing") val isFollowing: Boolean?, + @SerializedName("isBlock") val isBlock: Boolean +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt new file mode 100644 index 0000000..35169c4 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import androidx.recyclerview.widget.DiffUtil +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember + +class LiveRoomMemberResponseDiffUtilCallback( + private val oldData: List, + private val newData: List +) : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldData[oldItemPosition] + val newItem = newData[newItemPosition] + + return if (oldItem is LiveRoomMember && newItem is LiveRoomMember) { + oldItem.id == newItem.id + } else { + false + } + } + + override fun getOldListSize(): Int = oldData.size + + override fun getNewListSize(): Int = newData.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + oldData[oldItemPosition] == newData[newItemPosition] +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt new file mode 100644 index 0000000..491eee6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt @@ -0,0 +1,248 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomListProfileBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileHeaderBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileManagerBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileMasterBinding +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember + +class LiveRoomProfileAdapter( + private val isStaff: () -> Boolean, + private val onClickInviteSpeaker: (Long) -> Unit, + private val onClickChangeListener: (Long) -> Unit, + private val kickOut: (Long) -> Unit, + private val onClickProfile: (Long) -> Unit +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + var managerId: Long = -1 + var totalUserCount: Int = 0 + + @SuppressLint("NotifyDataSetChanged") + fun updateList( + speakers: List, + listeners: List, + managers: List + ) { + val items = mutableListOf() + speakers.forEach { + if (it.id == managerId) { + val item = LiveRoomProfileItemMaster( + nickname = it.nickname, + profileUrl = it.profileImage + ) + item.id = it.id + + items.add(0, item) + } else { + val item = LiveRoomProfileItemUser( + nickname = it.nickname, + profileUrl = it.profileImage, + role = it.role + ) + + item.id = it.id + items.add(item) + } + } + + items.add( + 1, + LiveRoomProfileItemSpeakerTitle( + "스피커", + speakerCount = speakers.size - 1, + totalUserCount = totalUserCount + ) + ) + + items.add( + 1, + LiveRoomProfileItemManagerTitle( + "스탭", + managerCount = managers.size + ) + ) + + managers.forEachIndexed { index, manager -> + val item = LiveRoomProfileItemManager( + nickname = manager.nickname, + profileUrl = manager.profileImage + ) + + item.id = manager.id + items.add(index = index + 2, item) + } + + items.add(LiveRoomProfileItemListenerTitle("리스너")) + + listeners.forEach { + val item = LiveRoomProfileItemUser( + nickname = it.nickname, + profileUrl = it.profileImage, + role = it.role + ) + + item.id = it.id + items.add(item) + } + + this.items.run { + clear() + addAll(items) + notifyDataSetChanged() + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveRoomProfileViewHolder { + when (viewType) { + LiveRoomProfileItemType.SPEAKER_TITLE.ordinal -> { + return LiveRoomProfileSpeakerTitleViewHolder( + ItemLiveRoomProfileHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + LiveRoomProfileItemType.MANAGER_TITLE.ordinal -> { + return LiveRoomProfileManagerTitleViewHolder( + ItemLiveRoomProfileHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + LiveRoomProfileItemType.LISTENER_TITLE.ordinal -> { + return LiveRoomProfileListenerTitleViewHolder( + ItemLiveRoomProfileHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + LiveRoomProfileItemType.MASTER.ordinal -> { + return LiveRoomProfileMasterViewHolder( + ItemLiveRoomProfileMasterBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + managerId, + onClickProfile = onClickProfile + ) + } + + LiveRoomProfileItemType.MANAGER.ordinal -> { + return LiveRoomProfileManagerViewHolder( + ItemLiveRoomProfileManagerBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + onClickProfile = onClickProfile + ) + } + + else -> { + return LiveRoomProfileUserViewHolder( + ItemLiveRoomListProfileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + isStaff = isStaff, + managerId = managerId, + onClickInviteSpeaker = onClickInviteSpeaker, + onClickChangeListener = onClickChangeListener, + kickOut = kickOut, + onClickProfile = onClickProfile + ) + } + } + } + + override fun onBindViewHolder(holder: LiveRoomProfileViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemViewType(position: Int): Int { + return items[position].type.ordinal + } + + override fun getItemCount(): Int = items.size +} + +abstract class LiveRoomProfileViewHolder(binding: ViewBinding) : + RecyclerView.ViewHolder(binding.root) { + abstract fun bind(item: LiveRoomProfileItem) +} + +class LiveRoomProfileSpeakerTitleViewHolder( + private val binding: ItemLiveRoomProfileHeaderBinding, +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) = item.bind(binding) +} + +class LiveRoomProfileListenerTitleViewHolder( + private val binding: ItemLiveRoomProfileHeaderBinding, +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) = item.bind(binding) +} + +class LiveRoomProfileManagerTitleViewHolder( + private val binding: ItemLiveRoomProfileHeaderBinding, +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) = item.bind(binding) +} + +class LiveRoomProfileMasterViewHolder( + private val binding: ItemLiveRoomProfileMasterBinding, + private val managerId: Long, + private val onClickProfile: (Long) -> Unit +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) { + item.onClickProfile = onClickProfile + item.managerId = managerId + item.bind(binding) + } +} + +class LiveRoomProfileManagerViewHolder( + private val binding: ItemLiveRoomProfileManagerBinding, + private val onClickProfile: (Long) -> Unit +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) { + item.onClickProfile = onClickProfile + item.bind(binding) + } +} + +class LiveRoomProfileUserViewHolder( + private val binding: ItemLiveRoomListProfileBinding, + private val isStaff: () -> Boolean, + private val managerId: Long, + private val onClickInviteSpeaker: (Long) -> Unit, + private val onClickChangeListener: (Long) -> Unit, + private val kickOut: (Long) -> Unit, + private val onClickProfile: (Long) -> Unit +) : LiveRoomProfileViewHolder(binding) { + override fun bind(item: LiveRoomProfileItem) { + item.isStaff = isStaff + item.managerId = managerId + item.onClickInviteSpeaker = onClickInviteSpeaker + item.onClickChangeListener = onClickChangeListener + item.kickOut = kickOut + item.onClickProfile = onClickProfile + item.bind(binding) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt new file mode 100644 index 0000000..478ccc5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt @@ -0,0 +1,71 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.LiveData +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialog +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomProfileBinding +import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse + +class LiveRoomProfileDialog( + layoutInflater: LayoutInflater, + private val activity: FragmentActivity, + private val roomInfoLiveData: LiveData, + isStaff: () -> Boolean, + onClickInviteSpeaker: (Long) -> Unit, + onClickChangeListener: (Long) -> Unit, + onClickKickOut: (Long) -> Unit, + onClickProfile: (Long) -> Unit +) { + private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity) + private val dialogView = DialogLiveRoomProfileBinding.inflate(layoutInflater) + private val adapter = LiveRoomProfileAdapter( + isStaff = isStaff, + onClickInviteSpeaker = onClickInviteSpeaker, + onClickChangeListener = onClickChangeListener, + kickOut = onClickKickOut, + onClickProfile = onClickProfile + ) + + init { + bottomSheetDialog.setContentView(dialogView.root) + bottomSheetDialog.setCancelable(false) + + dialogView.ivClose.setOnClickListener { bottomSheetDialog.dismiss() } + setupRecyclerView() + bindData() + } + + private fun setupRecyclerView() { + dialogView.rvPeoples.isNestedScrollingEnabled = true + dialogView.rvPeoples.layoutManager = LinearLayoutManager( + activity, + LinearLayoutManager.VERTICAL, + false + ) + + dialogView.rvPeoples.adapter = adapter + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + roomInfoLiveData.observe(activity) { + adapter.managerId = it.managerId + adapter.totalUserCount = it.totalAvailableParticipantsCount + dialogView.tvParticipate.text = "${it.participantsCount}" + dialogView.tvTotalPeoples.text = "/${it.totalAvailableParticipantsCount}" + + adapter.updateList( + speakers = it.speakerList, + listeners = it.listenerList, + managers = it.managerList + ) + } + } + + fun show() { + bottomSheetDialog.show() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt new file mode 100644 index 0000000..efff22e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt @@ -0,0 +1,201 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import coil.load +import coil.transform.CircleCropTransformation +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomListProfileBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileHeaderBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileManagerBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileMasterBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMemberRole + +enum class LiveRoomProfileItemType { + @SerializedName("MASTER") + MASTER, + + @SerializedName("MANAGER") + MANAGER, + + @SerializedName("SPEAKER_TITLE") + SPEAKER_TITLE, + + @SerializedName("LISTENER_TITLE") + LISTENER_TITLE, + + @SerializedName("MANAGER_TITLE") + MANAGER_TITLE, + + @SerializedName("USER") + USER +} + +abstract class LiveRoomProfileItem { + open var id: Long = 0L + open var type: LiveRoomProfileItemType = LiveRoomProfileItemType.USER + open var isStaff: () -> Boolean = { false } + open var managerId = 0L + open var onClickInviteSpeaker: (Long) -> Unit = {} + open var onClickChangeListener: (Long) -> Unit = {} + open var kickOut: (Long) -> Unit = {} + open var onClickProfile: (Long) -> Unit = {} + abstract fun bind(binding: ViewBinding) +} + +data class LiveRoomProfileItemSpeakerTitle( + val title: String, + val speakerCount: Int, + val totalUserCount: Int +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.SPEAKER_TITLE + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomProfileHeaderBinding + itemBinding.tvTitle.text = title + itemBinding.tvSpeakerCount.text = "$speakerCount" + itemBinding.tvSpeakerTotalCount.text = if (totalUserCount > 9) { + "/9" + } else { + "/${totalUserCount - 1}" + } + itemBinding.tvSpeakerCount.visibility = View.VISIBLE + itemBinding.tvSpeakerTotalCount.visibility = View.VISIBLE + + val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams + lp.topMargin = 14f.dpToPx().toInt() + lp.bottomMargin = 14f.dpToPx().toInt() + itemBinding.llRoot.layoutParams = lp + } +} + +data class LiveRoomProfileItemListenerTitle( + val title: String +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.LISTENER_TITLE + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomProfileHeaderBinding + itemBinding.tvTitle.text = title + itemBinding.tvSpeakerCount.visibility = View.GONE + itemBinding.tvSpeakerTotalCount.visibility = View.GONE + + val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams + lp.topMargin = 20f.dpToPx().toInt() + lp.bottomMargin = 14f.dpToPx().toInt() + itemBinding.llRoot.layoutParams = lp + } +} + +data class LiveRoomProfileItemManagerTitle( + val title: String, + val managerCount: Int +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.MANAGER_TITLE + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomProfileHeaderBinding + itemBinding.tvTitle.text = title + itemBinding.tvSpeakerCount.text = "$managerCount" + itemBinding.tvSpeakerCount.visibility = View.VISIBLE + itemBinding.tvSpeakerTotalCount.visibility = View.GONE + + val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams + lp.topMargin = 14f.dpToPx().toInt() + lp.bottomMargin = 14f.dpToPx().toInt() + itemBinding.llRoot.layoutParams = lp + } +} + +data class LiveRoomProfileItemMaster( + val nickname: String, + val profileUrl: String +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.MASTER + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomProfileMasterBinding + itemBinding.tvNickname.text = nickname + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + if (id != SharedPreferenceManager.userId) { + itemBinding.ivProfile.setOnClickListener { onClickProfile(id) } + } + } +} + +data class LiveRoomProfileItemManager( + val nickname: String, + val profileUrl: String +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.MANAGER + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomProfileManagerBinding + itemBinding.tvNickname.text = nickname + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + if (id != SharedPreferenceManager.userId) { + itemBinding.ivProfile.setOnClickListener { + onClickProfile(id) + } + } + } +} + +data class LiveRoomProfileItemUser( + val nickname: String, + val profileUrl: String, + val role: LiveRoomMemberRole +) : LiveRoomProfileItem() { + override var type = LiveRoomProfileItemType.USER + override fun bind(binding: ViewBinding) { + val itemBinding = binding as ItemLiveRoomListProfileBinding + itemBinding.tvNickname.text = nickname + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + if (id != SharedPreferenceManager.userId) { + itemBinding.ivProfile.setOnClickListener { onClickProfile(id) } + } + + if (id == SharedPreferenceManager.userId && role == LiveRoomMemberRole.SPEAKER) { + itemBinding.llControlButtons.visibility = View.VISIBLE + itemBinding.tvChangeAudience.visibility = View.VISIBLE + itemBinding.tvInviteSpeaker.visibility = View.GONE + itemBinding.ivKickOut.visibility = View.GONE + + itemBinding.tvChangeAudience.setOnClickListener { onClickChangeListener(id) } + itemBinding.tvInviteSpeaker.setOnClickListener { } + } else if (managerId == SharedPreferenceManager.userId || isStaff()) { + itemBinding.llControlButtons.visibility = View.VISIBLE + itemBinding.ivKickOut.visibility = View.VISIBLE + + if (role == LiveRoomMemberRole.SPEAKER) { + itemBinding.tvChangeAudience.visibility = View.VISIBLE + itemBinding.tvInviteSpeaker.visibility = View.GONE + + itemBinding.tvChangeAudience.setOnClickListener { onClickChangeListener(id) } + itemBinding.tvInviteSpeaker.setOnClickListener { } + } else { + itemBinding.tvChangeAudience.visibility = View.GONE + itemBinding.tvInviteSpeaker.visibility = View.VISIBLE + + itemBinding.tvChangeAudience.setOnClickListener { } + itemBinding.tvInviteSpeaker.setOnClickListener { onClickInviteSpeaker(id) } + } + + itemBinding.ivKickOut.setOnClickListener { kickOut(id) } + } else { + itemBinding.llControlButtons.visibility = View.GONE + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt new file mode 100644 index 0000000..078d558 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt @@ -0,0 +1,86 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember + +class LiveRoomProfileListAdapter : RecyclerView.Adapter() { + inner class ViewHolder( + private val binding: ItemLiveRoomProfileBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: LiveRoomMember) { + binding.ivProfile.load(item.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(23.3f.dpToPx())) + + if (activeSpeakers.contains(item.id.toInt())) { + binding.ivBg.visibility = View.VISIBLE + } else { + binding.ivBg.visibility = View.GONE + } + + if (muteSpeakers.contains(item.id.toInt())) { + binding.ivMute.visibility = View.VISIBLE + } else { + binding.ivMute.visibility = View.GONE + } + + val ivMuteLp = binding.ivMute.layoutParams + ivMuteLp.width = 51.7f.dpToPx().toInt() + ivMuteLp.height = 51.7f.dpToPx().toInt() + binding.ivMute.layoutParams = ivMuteLp + + if (managerId == item.id) { + binding.ivCrown.visibility = View.VISIBLE + } else { + binding.ivCrown.visibility = View.GONE + } + } + + binding.tvNickname.text = item.nickname + } + } + + private val items = mutableListOf() + val activeSpeakers = mutableSetOf() + val muteSpeakers = mutableSetOf() + var managerId: Long = -1 + + fun updateList(items: List) { + items.let { + val diffCallback = LiveRoomMemberResponseDiffUtilCallback(this.items, items) + val diffResult = DiffUtil.calculateDiff(diffCallback) + + this.items.run { + clear() + addAll(items) + diffResult.dispatchUpdatesTo(this@LiveRoomProfileListAdapter) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemLiveRoomProfileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: LiveRoomProfileListAdapter.ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt new file mode 100644 index 0000000..234898a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt @@ -0,0 +1,192 @@ +package kr.co.vividnext.sodalive.live.room.profile + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.view.Window +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.LiveData +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomUserProfileBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class LiveRoomUserProfileDialog( + private val activity: FragmentActivity, + private val userProfileLiveData: LiveData, + layoutInflater: LayoutInflater, + private val isStaff: (Long) -> Boolean, + private val onClickSendMessage: (Long, String) -> Unit, + private val onClickSetManager: (Long) -> Unit, + private val onClickReleaseManager: (Long) -> Unit, + private val onClickFollow: (Long) -> Unit, + private val onClickUnFollow: (Long) -> Unit, + private val onClickInviteSpeaker: (Long) -> Unit, + private val onClickChangeListener: (Long) -> Unit, + private val onClickKickOut: (Long) -> Unit, + private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit, +) { + private val alertDialog: AlertDialog + private val dialogView = DialogLiveRoomUserProfileBinding.inflate(layoutInflater) + + private var isIntroduceFold = true + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + setupView() + bindUserProfile() + } + + fun dismiss() { + alertDialog.dismiss() + } + + fun show() { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = activity.resources.displayMetrics.widthPixels - (40.dpToPx()).toInt() + lp.height = activity.resources.displayMetrics.heightPixels - (37.3f.dpToPx()).toInt() + + alertDialog.window?.attributes = lp + } + + fun isShowing(): Boolean { + return alertDialog.isShowing + } + + private fun setupView() { + val profileLP = dialogView.ivProfile.layoutParams + profileLP.width = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt() + profileLP.height = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt() + dialogView.ivProfile.layoutParams = profileLP + } + + private fun bindUserProfile() { + userProfileLiveData.observe(activity) { userProfile -> + dialogView.ivProfile.load(userProfile.profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(8.dpToPx())) + } + + dialogView.tvNickname.text = userProfile.nickname + dialogView.tvTags.text = userProfile.tags + dialogView.tvGender.text = userProfile.gender + dialogView.tvIntroduce.text = userProfile.introduce + + dialogView.ivClose.setOnClickListener { alertDialog.dismiss() } + dialogView.llSendMessage.setOnClickListener { + onClickSendMessage( + userProfile.userId, + userProfile.nickname + ) + } + + dialogView.tvIntroduce.setOnClickListener { + isIntroduceFold = !isIntroduceFold + + if (isIntroduceFold) { + dialogView.tvIntroduce.maxLines = 2 + } else { + dialogView.tvIntroduce.maxLines = Int.MAX_VALUE + } + } + + if (userProfile.isSpeaker != null) { + dialogView.tvInviteSpeaker.visibility = View.VISIBLE + + if (userProfile.isSpeaker) { + dialogView.tvInviteSpeaker.text = "리스너 변경" + dialogView.tvInviteSpeaker.setOnClickListener { + onClickChangeListener(userProfile.userId) + alertDialog.dismiss() + } + } else { + dialogView.tvInviteSpeaker.text = "스피커 초대" + dialogView.tvInviteSpeaker.setOnClickListener { + onClickInviteSpeaker(userProfile.userId) + alertDialog.dismiss() + } + } + } else { + dialogView.tvInviteSpeaker.visibility = View.GONE + dialogView.tvKickOut.visibility = View.GONE + } + + if (userProfile.isManager != null) { + dialogView.tvSetManager.visibility = View.VISIBLE + + if (userProfile.isManager) { + dialogView.tvSetManager.text = "스탭 해제" + dialogView.tvSetManager.setOnClickListener { + onClickReleaseManager(userProfile.userId) + alertDialog.dismiss() + } + } else { + dialogView.tvSetManager.text = "스탭 지정" + dialogView.tvSetManager.setOnClickListener { + onClickSetManager(userProfile.userId) + alertDialog.dismiss() + } + } + } else { + dialogView.tvSetManager.visibility = View.GONE + } + + if ( + (userProfile.isSpeaker != null && !isStaff(userProfile.userId)) || + (userProfile.isSpeaker != null && userProfile.isManager != null) + ) { + dialogView.tvKickOut.visibility = View.VISIBLE + dialogView.tvKickOut.setOnClickListener { + onClickKickOut(userProfile.userId) + alertDialog.dismiss() + } + } + + if (userProfile.isFollowing != null) { + dialogView.llFollow.visibility = View.VISIBLE + + if (userProfile.isFollowing) { + dialogView.tvFollow.text = "팔로잉" + dialogView.ivFollow.setImageResource(R.drawable.ic_alarm_selected) + dialogView.llFollow.setBackgroundResource( + R.drawable.bg_round_corner_23_3_3e1b93_9970ff + ) + dialogView.tvFollow.setOnClickListener { onClickUnFollow(userProfile.userId) } + } else { + dialogView.tvFollow.text = "팔로우" + dialogView.ivFollow.setImageResource(R.drawable.ic_alarm) + dialogView.llFollow.setBackgroundResource( + R.drawable.bg_round_corner_23_3_transparent_9970ff + ) + dialogView.tvFollow.setOnClickListener { onClickFollow(userProfile.userId) } + } + } else { + dialogView.llFollow.visibility = View.GONE + } + + dialogView.ivMenu.setOnClickListener { + onClickPopupMenu( + userProfile.userId, + userProfile.nickname, + userProfile.isBlock, + dialogView.ivMenu + ) + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt new file mode 100644 index 0000000..5b19e88 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt @@ -0,0 +1,88 @@ +package kr.co.vividnext.sodalive.live.room.update + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.view.LayoutInflater +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class LiveRoomInfoEditDialog( + activity: Activity, + layoutInflater: LayoutInflater, + onClickImagePicker: () -> Unit +) { + private val alertDialog: AlertDialog + private val dialogView = DialogLiveRoomInfoUpdateBinding.inflate(layoutInflater) + + private var coverImageUrl: String? = null + private var coverImageUri: Uri? = null + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() } + dialogView.ivClose.setOnClickListener { alertDialog.dismiss() } + dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() } + } + + fun setRoomInfo( + currentTitle: String, + currentContent: String, + ) { + dialogView.etTitle.setText(currentTitle) + dialogView.etContent.setText(currentContent) + } + + fun setCoverImageUri(coverImageUri: Uri) { + this.coverImageUri = coverImageUri + dialogView.ivCover.load(coverImageUri) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + } + + fun setCoverImageUrl(coverImageUrl: String) { + this.coverImageUrl = coverImageUrl + dialogView.ivCover.load(coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + } + + fun setConfirmAction(confirmAction: (String, String, Uri?) -> Unit) { + dialogView.tvConfirm.setOnClickListener { + alertDialog.dismiss() + + val newTitle = dialogView.etTitle.text.toString() + val newContent = dialogView.etContent.text.toString() + confirmAction(newTitle, newContent, coverImageUri) + coverImageUri = null + coverImageUrl = null + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt new file mode 100644 index 0000000..8801afe --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt @@ -0,0 +1,50 @@ +package kr.co.vividnext.sodalive.report + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogProfileReportBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class ProfileReportDialog( + activity: Activity, + layoutInflater: LayoutInflater, + confirmButtonClick: () -> Unit +) { + + private val alertDialog: AlertDialog + + val dialogView = DialogProfileReportBinding.inflate(layoutInflater) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvReport.setOnClickListener { + alertDialog.dismiss() + confirmButtonClick() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt new file mode 100644 index 0000000..e6cfcaf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.report + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + +interface ReportApi { + @POST("/report") + fun report( + @Body request: ReportRequest, + @Header("Authorization") authHeader: String + ): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt new file mode 100644 index 0000000..d1c3e3c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.report + +class ReportRepository(private val api: ReportApi) { + fun report(request: ReportRequest, token: String) = api.report(request, authHeader = token) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt new file mode 100644 index 0000000..24d8f13 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.report + +import com.google.gson.annotations.SerializedName + +data class ReportRequest( + @SerializedName("type") val type: ReportType, + @SerializedName("reason") val reason: String, + @SerializedName("reportedAccountId") val reportedAccountId: Long? = null, + @SerializedName("cheersId") val cheersId: Long? = null, + @SerializedName("audioContentId") val audioContentId: Long? = null, +) + +enum class ReportType { + @SerializedName("REVIEW") REVIEW, + @SerializedName("PROFILE") PROFILE, + @SerializedName("USER") USER, + @SerializedName("CHEERS") CHEERS, + @SerializedName("AUDIO_CONTENT") AUDIO_CONTENT +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt new file mode 100644 index 0000000..6b1a475 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt @@ -0,0 +1,58 @@ +package kr.co.vividnext.sodalive.report + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.RadioButton +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogUserReportBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class UserReportDialog( + activity: Activity, + layoutInflater: LayoutInflater, + confirmButtonClick: (String) -> Unit +) { + + private val alertDialog: AlertDialog + + val dialogView = DialogUserReportBinding.inflate(layoutInflater) + + var reason = "" + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvReport.setOnClickListener { + alertDialog.dismiss() + confirmButtonClick(reason) + } + + dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId -> + val radioButton = radioGroup.findViewById(checkedId) + reason = radioButton.text.toString() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt new file mode 100644 index 0000000..e0d9359 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.user + +import com.google.gson.annotations.SerializedName + +data class CreatorFollowRequestRequest(@SerializedName("creatorId") val creatorId: Long) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt index 931eea1..b451080 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.user import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest import kr.co.vividnext.sodalive.mypage.MyPageResponse import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse @@ -56,4 +57,28 @@ interface UserApi { @Query("container") container: String = "aos", @Header("Authorization") authHeader: String ): Single> + + @POST("/member/block") + fun memberBlock( + @Body request: MemberBlockRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/member/unblock") + fun memberUnBlock( + @Body request: MemberBlockRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/member/creator/follow") + fun creatorFollow( + request: Any, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/member/creator/unfollow") + fun creatorUnFollow( + request: Any, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt index 1948cc7..0c1143c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.user import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest import kr.co.vividnext.sodalive.mypage.MyPageResponse import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest @@ -35,4 +36,14 @@ class UserRepository(private val userApi: UserApi) { fun getMyPage(token: String): Single> { return userApi.getMyPage(authHeader = token) } + + fun memberBlock( + userId: Long, + token: String + ) = userApi.memberBlock(request = MemberBlockRequest(userId), authHeader = token) + + fun memberUnBlock( + userId: Long, + token: String + ) = userApi.memberUnBlock(request = MemberBlockRequest(userId), authHeader = token) } diff --git a/app/src/main/res/drawable-xxhdpi/btn_follow.png b/app/src/main/res/drawable-xxhdpi/btn_follow.png new file mode 100644 index 0000000000000000000000000000000000000000..df2215687c5604d3ea48146a674fbee0ea81a813 GIT binary patch literal 4311 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 LNkvXXu0mjf1rS`I literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..7d99d63d9982a15b1de5a556c4e50e407bab75f3 GIT binary patch literal 804 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation.png b/app/src/main/res/drawable-xxhdpi/ic_donation.png new file mode 100644 index 0000000000000000000000000000000000000000..3732210c52b6389136d1eff52779bbf4adcfb73c GIT binary patch literal 4898 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 z,YsCfLR&$T`I@=4o=kflgNjRfBlX)p(Q;jq%|oGwJx03s1iG?YN!Rqt!h|Dof^ zA5YnOUsqro-qDwlj^# cT(FAyKe3VRIth%a+yDRo07*qoM6N<$f(byazyJUM literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation_status.png b/app/src/main/res/drawable-xxhdpi/ic_donation_status.png new file mode 100644 index 0000000000000000000000000000000000000000..1e1dab5ed29f66f746b3b4930c84db5dd9a37019 GIT binary patch literal 6819 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 RykP(U002ovPDHLkV1k9)@BIJ( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation_white.png b/app/src/main/res/drawable-xxhdpi/ic_donation_white.png new file mode 100644 index 0000000000000000000000000000000000000000..bdc3e781017e24432b9307b9731cd3e98d61aa3a GIT binary patch literal 891 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di49o(aE{-7;jBn@sFT7+_UdZ~NNWH95H$zbKUnK7m50_Ph)2I&>=k3Q-%-lX#T<`KU8=kHo?$nm^({a4~& zBSE0?K(N4wIZH74%!K-PH{Ei~uT>uZW+cH=c16KVZzFSi?E*XH&cteyIL1DdX(!m=|O`r3IZ%7GwC|t_%&T-An zeIM>iMwb<3oi=+~EWE8})y1s!PmH%7=e^qE8ujV?q$-D!_#0v;*-!o6swdPQ?e^Y3 z$ii-deYltW-%j~Q(`Frda(7v_@~U)^GmSDArqoNVt*Ab7CQN^h+#f9eyLM2<@igYd3-b2MJoF5>vu;>E3#n`h&vb&}UBiS9pYuCm(z(emg#kZRR2mzdoR!rz}_8eS*nXtTG=aav0eO~iH z4x1yfI~`f{Y~0Q@1%I}sPMvkL>p>K!p}FdO<`sfvK?T3vO;`TCD`D-uZDIexCGYO- zxaE4oZO77opV!=sSh1G5KFGW*<(g}m!dVYT<@t4+q=lB&d99F7nX=w3;@dytKuF4E bU|97@eoo}khy| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit.png b/app/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1937d38b8258322f15871e88dff5822e0a3a77 GIT binary patch literal 491 zcmV!d28+W-*d*8i$p~B08V}zE)bD5U{8J%NQ}r95F)=x zBt&EhcqZ@8z2Ao#^Y!>$Y=n5gp8PNj_w&;H&P(U0kQI%K-XF3+LZ!H3P_dT`K$e;K%##Gx#UBy_e2F~kq4&(G|7Wg0ovq2 zaDYa65EP(Q9vlW}mIo&1@Z9piu9sk$DJAUZ&UJctU=A`kxE2j+RU6hRGnAo5nCmOS7Bg5L;$n(~0V zk$t%1M#rqR7~J01GW{-+;T3sgw6``vxg@ h;?h7qc;BE=ieK?dCyc4Ed?Nq=002ovPDHLkV1iSq$}<1} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_kick_out.png b/app/src/main/res/drawable-xxhdpi/ic_kick_out.png new file mode 100644 index 0000000000000000000000000000000000000000..05944396b6b4d6cd1ecb07e1b1e3ab0326d1ed95 GIT binary patch literal 2522 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_message_send.png b/app/src/main/res/drawable-xxhdpi/ic_message_send.png new file mode 100644 index 0000000000000000000000000000000000000000..3a797f8ae6de6e7e4c6122fa2a9f0bb09cc2d980 GIT binary patch literal 787 zcmV+u1MK{XP)+LR{m+nj^u4yuu8KYkZK$ zE$JwhTb}F0tAkVw^BeTLmbaY$2S>a)Xk2?hzc`uHuTf0$hd2wI9C6KZgcSaBTykVB z-=}b7QU@JeEBTJ%p5mtJpy-2>;OJV3q+)M6t`m1JFgN5juhnl}ew6c`G708_bgq>* zE-6~X@q><0An9BySyPp+_5Bbrd_sEHO7=@2>UYn-KpN&+dBdk<=ia_Ne83HzaNN*v zQd6I}_tm2O^a8n&_cYA4l6zC1giH;Y#WmBd1!+9j$cTw+0`|>Gd@Yx@9Onn; z61cgyg#sIaw;0ZshD}=+_k`)zg4|y=R7}ho)(!Q=(F)XGJuls{>xt1m4( zWS@}32i#CXc8)e~wUf?0X=??am>bSXoZYq+j?bArcBaiT3P5v1O1OkDvcmk6I%darq R*f#(G002ovPDHLkV1iY`ZHE8= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_off.png b/app/src/main/res/drawable-xxhdpi/ic_mic_off.png new file mode 100644 index 0000000000000000000000000000000000000000..9bc69ff9f56a3593ea4bf37e7596d3c46968f490 GIT binary patch literal 645 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|emUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIB<0X`wF?gc*oi38nilg&rf5C*QR~emvMnCX$ zaSZV|K6RS4H?yNa!}M;AkBp4m8wDJY7{(2=TO>A5Fn&i_YmKd0$$Zub*5`}{FA{hR2qf8xKz3+|sgq5p$HAWgvb!Nl-%c94oEZ*l6=Ypi{D88t)nD2cBG)4oqT<3SwZDULmP4 zp~*38k}8h?6N}8GCKiclo{mg1mzkJ%u&@s@(rKG_BxYKq)(_<&Rmr-I;>FW0U*9AGqQo@5)yx zyR%A%rvKc8VoO!uHC-V!q7mVt=S&R6TvGS#NzgU_?ZJ}h=#xI>o^g~xOj;6`qtpbZ z^$X1e=1gpQV#VFP#Nnv!janb>ZI2H!YOXl3$G+B}gmZfDony28Wq2;Ti1PC_p6h=h zw5RWs`}IDdJ^mBzwT#mF%c>1_BJmeQ%}BF_8<72RfKuaVW^&mu0c}|M&j^{m65l*6^Jud;v@Z44$rjF6*2UngBOn B5J&(3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_on.png b/app/src/main/res/drawable-xxhdpi/ic_mic_on.png new file mode 100644 index 0000000000000000000000000000000000000000..31ed38e7bf2c195f87e5c783c00e5e49eb4d40f5 GIT binary patch literal 564 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|emUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIC~0X`wF?gc*oi3Vrm?lJ*2@|6Vn1vAWFaDSJ=`+5g~=m`cFJ|EamTaXdLefwv^Sy3CrPzvhIsLPbY3Yrm|=L2KzbT!#*PdOz=>2~*O6-R09Pp4?mr zVA##HH;~oYi7{Mr7t=hgEi>{Qf(5?k zF>h)rSJ3Nfs(mjMAj+{o^FsCagcF)4m^&9f;kFOSzqNOF!?ewR9F#a1!t8m~{!8Sn zWX@Vw-8f~pypch{f~WiUm9LkSut-E*-ILYRnKsR4RSrGs7<*QdQ`?)# zfNR6^9Tf)F0+~I+5!YfI!Wl~LC(l^9*CWS*Vb|}w&K@_mI2_{Y7JM)F-s+^()}OLw z%nf47lMe4LpLfGZY`ScW9e4G$*?hqh?HJu7&pc(*TWPEIU|o^l&Aq!t8@6Bi70!UtNzBwjS`Cga~MCqWn}PvDe~kC TL+NW^gfV!!`njxgN@xNA2y*za literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_mute.png b/app/src/main/res/drawable-xxhdpi/ic_mute.png new file mode 100644 index 0000000000000000000000000000000000000000..9d88d4cc5334db11c189b6441ee2bb2246ef0b7f GIT binary patch literal 7811 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 VPz{v5>3IME002ovPDHLkV1ky4)&l?l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_noti.png b/app/src/main/res/drawable-xxhdpi/ic_noti.png new file mode 100644 index 0000000000000000000000000000000000000000..e03f3c5b891ff422d0ddda0f53448c65c5956f62 GIT binary patch literal 6381 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notice_normal.png b/app/src/main/res/drawable-xxhdpi/ic_notice_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..830880cfdd6abb25612360bccf4fe65b5fee121a GIT binary patch literal 2695 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 BG$Q~2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notice_selected.png b/app/src/main/res/drawable-xxhdpi/ic_notice_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..6820e27d363f74a04b6df98eb433d14aa16a82ef GIT binary patch literal 2262 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png b/app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..bc34f5939f80f9e491898cc6d548bd46864eafe7 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKx3?xrnI^qbVSkfJRf%L|H?mvmFKt5w}kh>GZ zx^prwfgGU#pAc8~0-xQxckh7#AX}qJ^gK|OwIs+dnBkf2rmE`qR((JLM^6{W5R22v z2@*;TCj%lF`?q9gWN^rxUFgsdZEfsvi($EmvPV=yOeRyCaDfzKzaZDNK!aJ=+%DhB fh?4frHeq2nV4~vuscPv*pkWN2u6{1-oD!M<@UTGB literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_share.png b/app/src/main/res/drawable-xxhdpi/ic_share.png new file mode 100644 index 0000000000000000000000000000000000000000..5181ef2a570d695ed930f8be298e05e3d6f5b1e5 GIT binary patch literal 950 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_speaker_on.png b/app/src/main/res/drawable-xxhdpi/ic_speaker_on.png new file mode 100644 index 0000000000000000000000000000000000000000..be48c2d04961f8ff8d8b196468b4340727b4c1b1 GIT binary patch literal 5950 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/img_noti_mute.png b/app/src/main/res/drawable-xxhdpi/img_noti_mute.png new file mode 100644 index 0000000000000000000000000000000000000000..8bb4056b4bfa9e6832f9a1aa97d21dd53e647ab0 GIT binary patch literal 23305 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 YKarcb28tY)8~^|S07*qoM6N<$f+m;W;{X5v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml b/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml new file mode 100644 index 0000000..5e84bf0 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml b/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml new file mode 100644 index 0000000..8d1d989 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_circle_4999e3.xml b/app/src/main/res/drawable/bg_circle_4999e3.xml new file mode 100644 index 0000000..79dd10a --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_4999e3.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml b/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml new file mode 100644 index 0000000..bc0b8e4 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml b/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml new file mode 100644 index 0000000..21c613d --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml b/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml new file mode 100644 index 0000000..1e0edf9 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml b/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml new file mode 100644 index 0000000..37e6bbe --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml b/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml new file mode 100644 index 0000000..9826cab --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml b/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml new file mode 100644 index 0000000..f0936ac --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml b/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml new file mode 100644 index 0000000..1f383f6 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml b/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml new file mode 100644 index 0000000..aaf3104 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml b/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml new file mode 100644 index 0000000..f8a0947 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_99525252.xml b/app/src/main/res/drawable/bg_round_corner_10_99525252.xml new file mode 100644 index 0000000..db8b84d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_99525252.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml new file mode 100644 index 0000000..e06556a --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml new file mode 100644 index 0000000..8fb6ac3 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml new file mode 100644 index 0000000..9a637ff --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml new file mode 100644 index 0000000..0b0de7d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml new file mode 100644 index 0000000..0056003 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml b/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml new file mode 100644 index 0000000..47b782e --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml b/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml new file mode 100644 index 0000000..7e189aa --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml new file mode 100644 index 0000000..a23adaa --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml b/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml new file mode 100644 index 0000000..d77e4d2 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml new file mode 100644 index 0000000..647cb8d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml b/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml new file mode 100644 index 0000000..8cd3ccc --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml b/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml new file mode 100644 index 0000000..b5f2288 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml b/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml new file mode 100644 index 0000000..540f1da --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml new file mode 100644 index 0000000..70de629 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml b/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml new file mode 100644 index 0000000..5331f73 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml b/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml new file mode 100644 index 0000000..e5e61ab --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml new file mode 100644 index 0000000..79162be --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml b/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml new file mode 100644 index 0000000..d3e7bc5 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml new file mode 100644 index 0000000..9066562 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml b/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml new file mode 100644 index 0000000..33a5f01 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml b/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml new file mode 100644 index 0000000..0c6c674 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml new file mode 100644 index 0000000..bd801e8 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml new file mode 100644 index 0000000..c8dc3b5 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml new file mode 100644 index 0000000..e5a67df --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml new file mode 100644 index 0000000..2a8089c --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml new file mode 100644 index 0000000..a7fe57d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml new file mode 100644 index 0000000..fc00014 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_8_222222.xml b/app/src/main/res/drawable/bg_round_corner_8_222222.xml new file mode 100644 index 0000000..a00cbf1 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_8_222222.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml b/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml new file mode 100644 index 0000000..aeb43de --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml b/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml new file mode 100644 index 0000000..28a06f3 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml b/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml new file mode 100644 index 0000000..c834f26 --- /dev/null +++ b/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_live_room.xml b/app/src/main/res/layout/activity_live_room.xml new file mode 100644 index 0000000..38fe6f8 --- /dev/null +++ b/app/src/main/res/layout/activity_live_room.xml @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room.xml b/app/src/main/res/layout/dialog_live_room.xml new file mode 100644 index 0000000..e3c912e --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_donation.xml b/app/src/main/res/layout/dialog_live_room_donation.xml new file mode 100644 index 0000000..b509bf8 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_donation.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_donation_message.xml b/app/src/main/res/layout/dialog_live_room_donation_message.xml new file mode 100644 index 0000000..09ed448 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_donation_message.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_donation_ranking.xml b/app/src/main/res/layout/dialog_live_room_donation_ranking.xml new file mode 100644 index 0000000..8f30cb8 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_donation_ranking.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_info_update.xml b/app/src/main/res/layout/dialog_live_room_info_update.xml new file mode 100644 index 0000000..f2ef649 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_info_update.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_profile.xml b/app/src/main/res/layout/dialog_live_room_profile.xml new file mode 100644 index 0000000..813f583 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_profile.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_live_room_user_profile.xml b/app/src/main/res/layout/dialog_live_room_user_profile.xml new file mode 100644 index 0000000..0e0d997 --- /dev/null +++ b/app/src/main/res/layout/dialog_live_room_user_profile.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_profile_report.xml b/app/src/main/res/layout/dialog_profile_report.xml new file mode 100644 index 0000000..508e0dc --- /dev/null +++ b/app/src/main/res/layout/dialog_profile_report.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_user_report.xml b/app/src/main/res/layout/dialog_user_report.xml new file mode 100644 index 0000000..8a476ae --- /dev/null +++ b/app/src/main/res/layout/dialog_user_report.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_chat.xml b/app/src/main/res/layout/item_live_room_chat.xml new file mode 100644 index 0000000..3bd9b08 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_chat.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_donation_message.xml b/app/src/main/res/layout/item_live_room_donation_message.xml new file mode 100644 index 0000000..e621120 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_donation_message.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_donation_ranking.xml b/app/src/main/res/layout/item_live_room_donation_ranking.xml new file mode 100644 index 0000000..7a8099b --- /dev/null +++ b/app/src/main/res/layout/item_live_room_donation_ranking.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_donation_status_chat.xml b/app/src/main/res/layout/item_live_room_donation_status_chat.xml new file mode 100644 index 0000000..7cca94f --- /dev/null +++ b/app/src/main/res/layout/item_live_room_donation_status_chat.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/item_live_room_join_chat.xml b/app/src/main/res/layout/item_live_room_join_chat.xml new file mode 100644 index 0000000..fbf9540 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_join_chat.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/item_live_room_list_profile.xml b/app/src/main/res/layout/item_live_room_list_profile.xml new file mode 100644 index 0000000..71030f4 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_list_profile.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_profile.xml b/app/src/main/res/layout/item_live_room_profile.xml new file mode 100644 index 0000000..759e211 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_profile.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_profile_header.xml b/app/src/main/res/layout/item_live_room_profile_header.xml new file mode 100644 index 0000000..292782d --- /dev/null +++ b/app/src/main/res/layout/item_live_room_profile_header.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_profile_manager.xml b/app/src/main/res/layout/item_live_room_profile_manager.xml new file mode 100644 index 0000000..8bfe131 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_profile_manager.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_room_profile_master.xml b/app/src/main/res/layout/item_live_room_profile_master.xml new file mode 100644 index 0000000..7dcb243 --- /dev/null +++ b/app/src/main/res/layout/item_live_room_profile_master.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/audio_content_detail_creator_menu.xml b/app/src/main/res/menu/audio_content_detail_creator_menu.xml new file mode 100644 index 0000000..e7ec1c2 --- /dev/null +++ b/app/src/main/res/menu/audio_content_detail_creator_menu.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/menu/audio_content_detail_user_menu.xml b/app/src/main/res/menu/audio_content_detail_user_menu.xml new file mode 100644 index 0000000..ebe6795 --- /dev/null +++ b/app/src/main/res/menu/audio_content_detail_user_menu.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/review_option_menu.xml b/app/src/main/res/menu/review_option_menu.xml new file mode 100644 index 0000000..2c0296c --- /dev/null +++ b/app/src/main/res/menu/review_option_menu.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/menu/user_profile_option_menu.xml b/app/src/main/res/menu/user_profile_option_menu.xml new file mode 100644 index 0000000..529a98c --- /dev/null +++ b/app/src/main/res/menu/user_profile_option_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/menu/user_profile_option_menu_2.xml b/app/src/main/res/menu/user_profile_option_menu_2.xml new file mode 100644 index 0000000..2747bf1 --- /dev/null +++ b/app/src/main/res/menu/user_profile_option_menu_2.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1d8a96e..516b49d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -31,11 +31,40 @@ #DD4500 #1F1734 #333333 + #A285EB + #3D2A6C + #FFDC00 + #4999E3 + #C25264 #B3909090 #88909090 #339970FF #7FE2E2E2 #4D9970FF - #A285EB + #44000000 + #26909090 + #99525252 + #CC555555 + #999970ff + #E6548F7D + #E62D7390 + #E64D6AA4 + #E659548F + #E6D38C38 + #E6D85E37 + #33FFFFFF + #303030 + #555555 + #3E1B93 + #88333333 + #1B1B1B + #6F3DEC + #9F9F9F + #DCDCDC + #C67E4A + #E5A578 + #E6A77A + #FFB600 + #99000000