메시지 페이지 추가
| @@ -139,4 +139,7 @@ dependencies { | |||||||
|     // agora |     // agora | ||||||
|     implementation "io.agora.rtc:voice-sdk:4.1.0-1" |     implementation "io.agora.rtc:voice-sdk:4.1.0-1" | ||||||
|     implementation 'io.agora.rtm:rtm-sdk:1.5.3' |     implementation 'io.agora.rtm:rtm-sdk:1.5.3' | ||||||
|  |  | ||||||
|  |     // sound visualizer | ||||||
|  |     implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -45,6 +45,9 @@ | |||||||
|         <activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" /> |         <activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" /> | ||||||
|         <activity android:name=".explorer.profile.CreatorNoticeWriteActivity" /> |         <activity android:name=".explorer.profile.CreatorNoticeWriteActivity" /> | ||||||
|         <activity android:name=".explorer.profile.follow.UserFollowerListActivity" /> |         <activity android:name=".explorer.profile.follow.UserFollowerListActivity" /> | ||||||
|  |         <activity android:name=".message.text.TextMessageWriteActivity" /> | ||||||
|  |         <activity android:name=".message.text.TextMessageDetailActivity" /> | ||||||
|  |         <activity android:name=".message.SelectMessageRecipientActivity" /> | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" |             android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" | ||||||
|   | |||||||
| @@ -17,10 +17,14 @@ object Constants { | |||||||
|     const val EXTRA_TERMS = "extra_terms" |     const val EXTRA_TERMS = "extra_terms" | ||||||
|     const val EXTRA_ROOM_ID = "extra_room_id" |     const val EXTRA_ROOM_ID = "extra_room_id" | ||||||
|     const val EXTRA_USER_ID = "extra_user_id" |     const val EXTRA_USER_ID = "extra_user_id" | ||||||
|  |     const val EXTRA_NICKNAME = "extra_nickname" | ||||||
|     const val EXTRA_MESSAGE_ID = "extra_message_id" |     const val EXTRA_MESSAGE_ID = "extra_message_id" | ||||||
|     const val EXTRA_ROOM_DETAIL = "extra_room_detail" |     const val EXTRA_ROOM_DETAIL = "extra_room_detail" | ||||||
|  |     const val EXTRA_MESSAGE_BOX = "extra_message_box" | ||||||
|  |     const val EXTRA_TEXT_MESSAGE = "extra_text_message" | ||||||
|     const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now" |     const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now" | ||||||
|     const val EXTRA_PREV_LIVE_ROOM = "extra_prev_live_room" |     const val EXTRA_PREV_LIVE_ROOM = "extra_prev_live_room" | ||||||
|  |     const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient" | ||||||
|     const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name" |     const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name" | ||||||
|     const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" |     const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,6 +22,13 @@ 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.tag.LiveTagViewModel | ||||||
| import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel | import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel | ||||||
| import kr.co.vividnext.sodalive.main.MainViewModel | import kr.co.vividnext.sodalive.main.MainViewModel | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageApi | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageRepository | ||||||
|  | import kr.co.vividnext.sodalive.message.SelectMessageRecipientViewModel | ||||||
|  | import kr.co.vividnext.sodalive.message.text.TextMessageViewModel | ||||||
|  | import kr.co.vividnext.sodalive.message.text.TextMessageWriteViewModel | ||||||
|  | import kr.co.vividnext.sodalive.message.voice.VoiceMessageViewModel | ||||||
|  | import kr.co.vividnext.sodalive.message.voice.VoiceMessageWriteViewModel | ||||||
| import kr.co.vividnext.sodalive.mypage.MyPageViewModel | import kr.co.vividnext.sodalive.mypage.MyPageViewModel | ||||||
| import kr.co.vividnext.sodalive.mypage.auth.AuthApi | import kr.co.vividnext.sodalive.mypage.auth.AuthApi | ||||||
| import kr.co.vividnext.sodalive.mypage.auth.AuthRepository | import kr.co.vividnext.sodalive.mypage.auth.AuthRepository | ||||||
| @@ -94,6 +101,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         single { ApiBuilder().build(get(), ReportApi::class.java) } |         single { ApiBuilder().build(get(), ReportApi::class.java) } | ||||||
|         single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } |         single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } | ||||||
|         single { ApiBuilder().build(get(), ExplorerApi::class.java) } |         single { ApiBuilder().build(get(), ExplorerApi::class.java) } | ||||||
|  |         single { ApiBuilder().build(get(), MessageApi::class.java) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val viewModelModule = module { |     private val viewModelModule = module { | ||||||
| @@ -116,6 +124,11 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         viewModel { ExplorerViewModel(get()) } |         viewModel { ExplorerViewModel(get()) } | ||||||
|         viewModel { UserProfileViewModel(get(), get(), get()) } |         viewModel { UserProfileViewModel(get(), get(), get()) } | ||||||
|         viewModel { UserFollowerListViewModel(get(), get()) } |         viewModel { UserFollowerListViewModel(get(), get()) } | ||||||
|  |         viewModel { TextMessageViewModel(get()) } | ||||||
|  |         viewModel { TextMessageWriteViewModel(get()) } | ||||||
|  |         viewModel { VoiceMessageViewModel(get()) } | ||||||
|  |         viewModel { VoiceMessageWriteViewModel(get()) } | ||||||
|  |         viewModel { SelectMessageRecipientViewModel(get(), get()) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val repositoryModule = module { |     private val repositoryModule = module { | ||||||
| @@ -129,6 +142,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | |||||||
|         factory { LiveTagRepository(get()) } |         factory { LiveTagRepository(get()) } | ||||||
|         factory { ReportRepository(get()) } |         factory { ReportRepository(get()) } | ||||||
|         factory { ExplorerRepository(get()) } |         factory { ExplorerRepository(get()) } | ||||||
|  |         factory { MessageRepository(get()) } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val moduleList = listOf( |     private val moduleList = listOf( | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog | |||||||
| import kr.co.vividnext.sodalive.databinding.FragmentExplorerBinding | import kr.co.vividnext.sodalive.databinding.FragmentExplorerBinding | ||||||
| import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity | import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity | ||||||
| import kr.co.vividnext.sodalive.extensions.dpToPx | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
| import kr.co.vividnext.sodalive.message.MessageSelectRecipientAdapter | import kr.co.vividnext.sodalive.message.SelectMessageRecipientAdapter | ||||||
| import org.koin.android.ext.android.inject | import org.koin.android.ext.android.inject | ||||||
| import java.util.concurrent.TimeUnit | import java.util.concurrent.TimeUnit | ||||||
|  |  | ||||||
| @@ -36,7 +36,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>( | |||||||
|     private lateinit var imm: InputMethodManager |     private lateinit var imm: InputMethodManager | ||||||
|  |  | ||||||
|     private val handler = Handler(Looper.getMainLooper()) |     private val handler = Handler(Looper.getMainLooper()) | ||||||
|     private lateinit var searchChannelAdapter: MessageSelectRecipientAdapter |     private lateinit var searchChannelAdapter: SelectMessageRecipientAdapter | ||||||
|  |  | ||||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
| @@ -106,7 +106,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun setupSearchChannelView() { |     private fun setupSearchChannelView() { | ||||||
|         searchChannelAdapter = MessageSelectRecipientAdapter { |         searchChannelAdapter = SelectMessageRecipientAdapter { | ||||||
|             hideKeyboard() |             hideKeyboard() | ||||||
|             val intent = Intent(requireContext(), UserProfileActivity::class.java) |             val intent = Intent(requireContext(), UserProfileActivity::class.java) | ||||||
|             intent.putExtra(Constants.EXTRA_USER_ID, it.id) |             intent.putExtra(Constants.EXTRA_USER_ID, it.id) | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ 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.CreateLiveRoomResponse | ||||||
| import kr.co.vividnext.sodalive.live.room.create.GetRecentRoomInfoResponse | 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.detail.GetRoomDetailResponse | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
| import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage | 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.GetLiveRoomDonationStatusResponse | ||||||
| import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse | import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse | ||||||
| @@ -182,4 +183,9 @@ interface LiveApi { | |||||||
|         @Path("id") id: Long, |         @Path("id") id: Long, | ||||||
|         @Header("Authorization") authHeader: String |         @Header("Authorization") authHeader: String | ||||||
|     ): Single<ApiResponse<GetLiveRoomDonationStatusResponse>> |     ): Single<ApiResponse<GetLiveRoomDonationStatusResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/live/room/recent_visit_room/users") | ||||||
|  |     fun recentVisitRoomUsers( | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<List<GetRoomDetailUser>>> | ||||||
| } | } | ||||||
|   | |||||||
| @@ -208,4 +208,6 @@ class LiveRepository( | |||||||
|         roomId: Long, |         roomId: Long, | ||||||
|         token: String |         token: String | ||||||
|     ) = api.donationStatus(roomId, authHeader = token) |     ) = api.donationStatus(roomId, authHeader = token) | ||||||
|  |  | ||||||
|  |     fun recentVisitRoomUsers(token: String) = api.recentVisitRoomUsers(authHeader = token) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import android.os.Parcelable | ||||||
|  | import com.google.gson.annotations.SerializedName | ||||||
|  | import kotlinx.parcelize.Parcelize | ||||||
|  |  | ||||||
|  | data class GetVoiceMessageResponse( | ||||||
|  |     @SerializedName("totalCount") val totalCount: Int, | ||||||
|  |     @SerializedName("items") val items: List<VoiceMessageItem> | ||||||
|  | ) { | ||||||
|  |     data class VoiceMessageItem( | ||||||
|  |         @SerializedName("messageId") val messageId: Long, | ||||||
|  |         @SerializedName("senderId") val senderId: Long, | ||||||
|  |         @SerializedName("senderNickname") val senderNickname: String, | ||||||
|  |         @SerializedName("senderProfileImageUrl") val senderProfileImageUrl: String, | ||||||
|  |         @SerializedName("recipientNickname") val recipientNickname: String, | ||||||
|  |         @SerializedName("recipientProfileImageUrl") val recipientProfileImageUrl: String, | ||||||
|  |         @SerializedName("voiceMessageUrl") val voiceMessageUrl: String, | ||||||
|  |         @SerializedName("date") val date: String, | ||||||
|  |         @SerializedName("isKept") val isKept: Boolean | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | data class GetTextMessageResponse( | ||||||
|  |     @SerializedName("totalCount") val totalCount: Int, | ||||||
|  |     @SerializedName("items") val items: List<TextMessageItem> | ||||||
|  | ) { | ||||||
|  |     @Parcelize | ||||||
|  |     data class TextMessageItem( | ||||||
|  |         @SerializedName("messageId") val messageId: Long, | ||||||
|  |         @SerializedName("senderId") val senderId: Long, | ||||||
|  |         @SerializedName("senderNickname") val senderNickname: String, | ||||||
|  |         @SerializedName("senderProfileImageUrl") val senderProfileImageUrl: String, | ||||||
|  |         @SerializedName("recipientNickname") val recipientNickname: String, | ||||||
|  |         @SerializedName("recipientProfileImageUrl") val recipientProfileImageUrl: String, | ||||||
|  |         @SerializedName("textMessage") val textMessage: String, | ||||||
|  |         @SerializedName("date") val date: String, | ||||||
|  |         @SerializedName("isKept") val isKept: Boolean | ||||||
|  |     ) : Parcelable | ||||||
|  | } | ||||||
							
								
								
									
										100
									
								
								app/src/main/java/kr/co/vividnext/sodalive/message/MessageApi.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,100 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import io.reactivex.rxjava3.core.Single | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import okhttp3.MultipartBody | ||||||
|  | import okhttp3.RequestBody | ||||||
|  | import retrofit2.http.Body | ||||||
|  | import retrofit2.http.DELETE | ||||||
|  | import retrofit2.http.GET | ||||||
|  | import retrofit2.http.Header | ||||||
|  | import retrofit2.http.Multipart | ||||||
|  | import retrofit2.http.POST | ||||||
|  | import retrofit2.http.PUT | ||||||
|  | import retrofit2.http.Part | ||||||
|  | import retrofit2.http.Path | ||||||
|  | import retrofit2.http.Query | ||||||
|  |  | ||||||
|  | interface MessageApi { | ||||||
|  |     @POST("/message/send/text") | ||||||
|  |     fun sendTextMessage( | ||||||
|  |         @Body request: SendTextMessageRequest, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<Any>> | ||||||
|  |  | ||||||
|  |     @POST("/message/send/voice") | ||||||
|  |     @Multipart | ||||||
|  |     fun sendVoiceMessage( | ||||||
|  |         @Part voiceFile: MultipartBody.Part, | ||||||
|  |         @Part("request") request: RequestBody, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<Any>> | ||||||
|  |  | ||||||
|  |     @GET("/message/sent/text") | ||||||
|  |     fun getSentTextMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/message/received/text") | ||||||
|  |     fun getReceivedTextMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/message/keep/text") | ||||||
|  |     fun getKeepTextMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/message/sent/voice") | ||||||
|  |     fun getSentVoiceMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/message/received/voice") | ||||||
|  |     fun getReceivedVoiceMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> | ||||||
|  |  | ||||||
|  |     @GET("/message/keep/voice") | ||||||
|  |     fun getKeepVoiceMessage( | ||||||
|  |         @Query("timezone") timezone: String, | ||||||
|  |         @Query("page") page: Int, | ||||||
|  |         @Query("size") size: Int, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> | ||||||
|  |  | ||||||
|  |     @DELETE("/message/{messageId}") | ||||||
|  |     fun deleteMessage( | ||||||
|  |         @Path("messageId") messageId: Long, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<Any>> | ||||||
|  |  | ||||||
|  |     @PUT("/message/keep/text/{id}") | ||||||
|  |     fun keepTextMessage( | ||||||
|  |         @Path("id") id: Long, | ||||||
|  |         @Body container: String = "aos", | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<Any>> | ||||||
|  |  | ||||||
|  |     @PUT("/message/keep/voice/{id}") | ||||||
|  |     fun keepVoiceMessage( | ||||||
|  |         @Path("id") id: Long, | ||||||
|  |         @Body container: String = "aos", | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<Any>> | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import com.google.gson.annotations.SerializedName | ||||||
|  |  | ||||||
|  | enum class MessageBox { | ||||||
|  |     @SerializedName("SENT") | ||||||
|  |     SENT, | ||||||
|  |  | ||||||
|  |     @SerializedName("RECEIVE") | ||||||
|  |     RECEIVE, | ||||||
|  |  | ||||||
|  |     @SerializedName("KEEP") | ||||||
|  |     KEEP | ||||||
|  | } | ||||||
| @@ -1,7 +1,65 @@ | |||||||
| package kr.co.vividnext.sodalive.message | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import com.google.android.material.tabs.TabLayout | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
| import kr.co.vividnext.sodalive.base.BaseFragment | import kr.co.vividnext.sodalive.base.BaseFragment | ||||||
| import kr.co.vividnext.sodalive.databinding.FragmentMessageBinding | import kr.co.vividnext.sodalive.databinding.FragmentMessageBinding | ||||||
|  | import kr.co.vividnext.sodalive.message.text.TextMessageFragment | ||||||
|  | import kr.co.vividnext.sodalive.message.voice.VoiceMessageFragment | ||||||
|  |  | ||||||
| class MessageFragment : BaseFragment<FragmentMessageBinding>(FragmentMessageBinding::inflate) { | class MessageFragment : BaseFragment<FragmentMessageBinding>(FragmentMessageBinding::inflate) { | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         setupView() | ||||||
|  |         changeFragment("message") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupView() { | ||||||
|  |         val tabs = binding.tabs | ||||||
|  |         tabs.addTab(tabs.newTab().setText("문자").setTag("message")) | ||||||
|  |         tabs.addTab(tabs.newTab().setText("음성").setTag("voice")) | ||||||
|  |  | ||||||
|  |         tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { | ||||||
|  |             override fun onTabSelected(tab: TabLayout.Tab) { | ||||||
|  |                 val tag = tab.tag as String | ||||||
|  |                 changeFragment(tag) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onTabUnselected(tab: TabLayout.Tab) { | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onTabReselected(tab: TabLayout.Tab) { | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun changeFragment(tag: String) { | ||||||
|  |         val fragmentManager = childFragmentManager | ||||||
|  |         val fragmentTransaction = fragmentManager.beginTransaction() | ||||||
|  |  | ||||||
|  |         val currentFragment = fragmentManager.primaryNavigationFragment | ||||||
|  |         if (currentFragment != null) { | ||||||
|  |             fragmentTransaction.hide(currentFragment) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var fragment = fragmentManager.findFragmentByTag(tag) | ||||||
|  |         if (fragment == null) { | ||||||
|  |             fragment = if (tag == "message") { | ||||||
|  |                 TextMessageFragment() | ||||||
|  |             } else { | ||||||
|  |                 VoiceMessageFragment() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             fragmentTransaction.add(R.id.container, fragment, tag) | ||||||
|  |         } else { | ||||||
|  |             fragmentTransaction.show(fragment) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fragmentTransaction.setPrimaryNavigationFragment(fragment) | ||||||
|  |         fragmentTransaction.setReorderingAllowed(true) | ||||||
|  |         fragmentTransaction.commitNow() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,124 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import io.reactivex.rxjava3.core.Single | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import okhttp3.MultipartBody | ||||||
|  | import okhttp3.RequestBody | ||||||
|  | import java.util.TimeZone | ||||||
|  |  | ||||||
|  | class MessageRepository(private val api: MessageApi) { | ||||||
|  |     fun sendTextMessage(request: SendTextMessageRequest, token: String): Single<ApiResponse<Any>> { | ||||||
|  |         return api.sendTextMessage(request, authHeader = token) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun sendVoiceMessage( | ||||||
|  |         voiceFile: MultipartBody.Part, | ||||||
|  |         request: RequestBody, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<Any>> { | ||||||
|  |         return api.sendVoiceMessage( | ||||||
|  |             voiceFile, | ||||||
|  |             request, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getSentTextMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> { | ||||||
|  |         return api.getSentTextMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getReceivedTextMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> { | ||||||
|  |         return api.getReceivedTextMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun keepTextMessage(messageId: Long, token: String): Single<ApiResponse<Any>> { | ||||||
|  |         return api.keepTextMessage( | ||||||
|  |             messageId, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getKeepTextMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetTextMessageResponse>> { | ||||||
|  |         return api.getKeepTextMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getSentVoiceMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> { | ||||||
|  |         return api.getSentVoiceMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getReceivedVoiceMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> { | ||||||
|  |         return api.getReceivedVoiceMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getKeepVoiceMessage( | ||||||
|  |         page: Int, | ||||||
|  |         size: Int, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<GetVoiceMessageResponse>> { | ||||||
|  |         return api.getKeepVoiceMessage( | ||||||
|  |             TimeZone.getDefault().id, | ||||||
|  |             page, | ||||||
|  |             size, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun deleteMessage( | ||||||
|  |         messageId: Long, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<Any>> { | ||||||
|  |         return api.deleteMessage(messageId, authHeader = token) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun keepVoiceMessage(messageId: Long, token: String): Single<ApiResponse<Any>> { | ||||||
|  |         return api.keepVoiceMessage( | ||||||
|  |             messageId, | ||||||
|  |             authHeader = token | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,91 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.jakewharton.rxbinding4.widget.textChanges | ||||||
|  | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.rxjava3.schedulers.Schedulers | ||||||
|  | import kr.co.vividnext.sodalive.base.BaseActivity | ||||||
|  | import kr.co.vividnext.sodalive.common.Constants | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ActivitySelectMessageRecipientBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  |  | ||||||
|  | class SelectMessageRecipientActivity : BaseActivity<ActivitySelectMessageRecipientBinding>( | ||||||
|  |     ActivitySelectMessageRecipientBinding::inflate | ||||||
|  | ) { | ||||||
|  |  | ||||||
|  |     private val viewModel: SelectMessageRecipientViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var adapter: SelectMessageRecipientAdapter | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         bindData() | ||||||
|  |         viewModel.searchUser("") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setupView() { | ||||||
|  |         binding.toolbar.tvBack.text = "받는 사람 검색" | ||||||
|  |         binding.toolbar.tvBack.setOnClickListener { finish() } | ||||||
|  |  | ||||||
|  |         val recyclerView = binding.rvRecipient | ||||||
|  |  | ||||||
|  |         adapter = SelectMessageRecipientAdapter { | ||||||
|  |             val intent = Intent() | ||||||
|  |             intent.putExtra(Constants.EXTRA_SELECT_RECIPIENT, it) | ||||||
|  |             setResult(RESULT_OK, intent) | ||||||
|  |             finish() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         recyclerView.layoutManager = LinearLayoutManager( | ||||||
|  |             applicationContext, | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         recyclerView.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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         recyclerView.adapter = adapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     private fun bindData() { | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             binding.etSearchNickname.textChanges().skip(1) | ||||||
|  |                 .debounce(500, TimeUnit.MILLISECONDS) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .subscribe { | ||||||
|  |                     viewModel.searchUser(it.toString()) | ||||||
|  |                 } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         viewModel.searchUserLiveData.observe(this) { | ||||||
|  |             adapter.items.clear() | ||||||
|  |             adapter.items.addAll(it) | ||||||
|  |             adapter.notifyDataSetChanged() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,9 +10,9 @@ import kr.co.vividnext.sodalive.databinding.ItemSelectRecipientBinding | |||||||
| import kr.co.vividnext.sodalive.extensions.dpToPx | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
| import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
| 
 | 
 | ||||||
| class MessageSelectRecipientAdapter( | class SelectMessageRecipientAdapter( | ||||||
|     private val onClickItem: (GetRoomDetailUser) -> Unit |     private val onClickItem: (GetRoomDetailUser) -> Unit | ||||||
| ) : RecyclerView.Adapter<MessageSelectRecipientAdapter.ViewHolder>() { | ) : RecyclerView.Adapter<SelectMessageRecipientAdapter.ViewHolder>() { | ||||||
|     inner class ViewHolder( |     inner class ViewHolder( | ||||||
|         private val binding: ItemSelectRecipientBinding |         private val binding: ItemSelectRecipientBinding | ||||||
|     ) : RecyclerView.ViewHolder(binding.root) { |     ) : RecyclerView.ViewHolder(binding.root) { | ||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
|  | import kr.co.vividnext.sodalive.user.UserRepository | ||||||
|  |  | ||||||
|  | class SelectMessageRecipientViewModel( | ||||||
|  |     private val liveRepository: LiveRepository, | ||||||
|  |     private val userRepository: UserRepository | ||||||
|  | ) : BaseViewModel() { | ||||||
|  |  | ||||||
|  |     private val _searchUserLiveData = | ||||||
|  |         MutableLiveData<List<GetRoomDetailUser>>() | ||||||
|  |     val searchUserLiveData: LiveData<List<GetRoomDetailUser>> | ||||||
|  |         get() = _searchUserLiveData | ||||||
|  |  | ||||||
|  |     fun searchUser(nickname: String) { | ||||||
|  |         if (nickname.length > 1) { | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 userRepository.searchUser(nickname, "Bearer ${SharedPreferenceManager.token}") | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 _searchUserLiveData.postValue(it.data!!) | ||||||
|  |                             } else { | ||||||
|  |                                 _searchUserLiveData.postValue(emptyList()) | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _searchUserLiveData.postValue(emptyList()) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 liveRepository.recentVisitRoomUsers("Bearer ${SharedPreferenceManager.token}") | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 _searchUserLiveData.postValue(it.data!!) | ||||||
|  |                             } else { | ||||||
|  |                                 _searchUserLiveData.postValue(emptyList()) | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _searchUserLiveData.postValue(emptyList()) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message | ||||||
|  |  | ||||||
|  | import com.google.gson.annotations.SerializedName | ||||||
|  |  | ||||||
|  | data class SendVoiceMessageRequest( | ||||||
|  |     @SerializedName("recipientId") val recipientId: Long, | ||||||
|  |     @SerializedName("container") val container: String = "aos" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | data class SendTextMessageRequest( | ||||||
|  |     @SerializedName("recipientId") val recipientId: Long, | ||||||
|  |     @SerializedName("textMessage") val textMessage: String, | ||||||
|  |     @SerializedName("container") val container: String = "aos" | ||||||
|  | ) | ||||||
| @@ -0,0 +1,63 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import coil.load | ||||||
|  | import coil.transform.RoundedCornersTransformation | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ItemTextMessageBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import kr.co.vividnext.sodalive.message.GetTextMessageResponse | ||||||
|  |  | ||||||
|  | class TextMessageAdapter( | ||||||
|  |     private val onItemClick: (GetTextMessageResponse.TextMessageItem) -> Unit | ||||||
|  | ) : RecyclerView.Adapter<TextMessageAdapter.ViewHolder>() { | ||||||
|  |  | ||||||
|  |     inner class ViewHolder( | ||||||
|  |         private val binding: ItemTextMessageBinding | ||||||
|  |     ) : RecyclerView.ViewHolder(binding.root) { | ||||||
|  |  | ||||||
|  |         fun bind(item: GetTextMessageResponse.TextMessageItem) { | ||||||
|  |             if (SharedPreferenceManager.nickname == item.recipientNickname) { | ||||||
|  |                 binding.ivProfile.load(item.senderProfileImageUrl) { | ||||||
|  |                     crossfade(true) | ||||||
|  |                     placeholder(R.drawable.bg_placeholder) | ||||||
|  |                     transformations(RoundedCornersTransformation(23.4f.dpToPx())) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 binding.tvNickname.text = item.senderNickname | ||||||
|  |             } else { | ||||||
|  |                 binding.ivProfile.load(item.recipientProfileImageUrl) { | ||||||
|  |                     crossfade(true) | ||||||
|  |                     placeholder(R.drawable.bg_placeholder) | ||||||
|  |                     transformations(RoundedCornersTransformation(23.4f.dpToPx())) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 binding.tvNickname.text = item.recipientNickname | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.tvDate.text = item.date | ||||||
|  |             binding.tvMessage.text = item.textMessage | ||||||
|  |  | ||||||
|  |             binding.root.setOnClickListener { onItemClick(item) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val items = mutableListOf<GetTextMessageResponse.TextMessageItem>() | ||||||
|  |  | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( | ||||||
|  |         ItemTextMessageBinding.inflate( | ||||||
|  |             LayoutInflater.from(parent.context), | ||||||
|  |             parent, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         holder.bind(items[position]) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount() = items.size | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.base.BaseActivity | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ActivityTextMessageDetailBinding | ||||||
|  |  | ||||||
|  | class TextMessageDetailActivity : BaseActivity<ActivityTextMessageDetailBinding>( | ||||||
|  |     ActivityTextMessageDetailBinding::inflate | ||||||
|  | ) { | ||||||
|  |     override fun setupView() { | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,229 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.base.BaseFragment | ||||||
|  | import kr.co.vividnext.sodalive.common.Constants | ||||||
|  | import kr.co.vividnext.sodalive.common.LoadingDialog | ||||||
|  | import kr.co.vividnext.sodalive.databinding.FragmentTextMessageBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageBox | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  |  | ||||||
|  | class TextMessageFragment : BaseFragment<FragmentTextMessageBinding>( | ||||||
|  |     FragmentTextMessageBinding::inflate | ||||||
|  | ) { | ||||||
|  |     private val viewModel: TextMessageViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |     private lateinit var adapter: TextMessageAdapter | ||||||
|  |  | ||||||
|  |     private lateinit var loadingDialog: LoadingDialog | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         activityResultLauncher = registerForActivityResult( | ||||||
|  |             ActivityResultContracts.StartActivityForResult() | ||||||
|  |         ) { | ||||||
|  |             if (it.resultCode == Activity.RESULT_OK) { | ||||||
|  |                 viewModel.page = 1 | ||||||
|  |                 viewModel.getMessages() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         setupView() | ||||||
|  |         bindData() | ||||||
|  |  | ||||||
|  |         viewModel.getMessages() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupView() { | ||||||
|  |         loadingDialog = LoadingDialog(requireActivity(), layoutInflater) | ||||||
|  |  | ||||||
|  |         binding.tvSent.setOnClickListener { | ||||||
|  |             viewModel.selectMessageBox(MessageBox.SENT) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvReceive.setOnClickListener { | ||||||
|  |             viewModel.selectMessageBox(MessageBox.RECEIVE) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvKeep.setOnClickListener { | ||||||
|  |             viewModel.selectMessageBox(MessageBox.KEEP) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivWrite.setOnClickListener { | ||||||
|  |             val intent = Intent(requireActivity(), TextMessageWriteActivity::class.java) | ||||||
|  |             activityResultLauncher.launch(intent) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val recyclerView = binding.rvMessage | ||||||
|  |         adapter = TextMessageAdapter { | ||||||
|  |             val intent = Intent(requireActivity(), TextMessageDetailActivity::class.java) | ||||||
|  |             intent.putExtra(Constants.EXTRA_TEXT_MESSAGE, it) | ||||||
|  |             intent.putExtra( | ||||||
|  |                 Constants.EXTRA_MESSAGE_BOX, | ||||||
|  |                 viewModel.messageBoxLiveData.value!!.name | ||||||
|  |             ) | ||||||
|  |             activityResultLauncher.launch(intent) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         recyclerView.layoutManager = LinearLayoutManager( | ||||||
|  |             requireContext(), | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         recyclerView.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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         recyclerView.adapter = adapter | ||||||
|  |  | ||||||
|  |         recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |  | ||||||
|  |                 val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! | ||||||
|  |                     .findLastCompletelyVisibleItemPosition() | ||||||
|  |                 val itemTotalCount = recyclerView.adapter!!.itemCount - 1 | ||||||
|  |  | ||||||
|  |                 // 스크롤이 끝에 도달했는지 확인 | ||||||
|  |                 if (!recyclerView.canScrollVertically(1) && | ||||||
|  |                     lastVisibleItemPosition == itemTotalCount | ||||||
|  |                 ) { | ||||||
|  |                     viewModel.getMessages(viewModel.messageBoxLiveData.value!!) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     private fun bindData() { | ||||||
|  |         viewModel.toastLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.isLoading.observe(viewLifecycleOwner) { | ||||||
|  |             if (it) { | ||||||
|  |                 loadingDialog.show(screenWidth, "문자 메시지를 불러오고 있습니다.") | ||||||
|  |             } else { | ||||||
|  |                 loadingDialog.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.getMessagesLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             if (viewModel.page - 1 == 1) { | ||||||
|  |                 adapter.items.clear() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (adapter.items.size == 0 && it.isEmpty()) { | ||||||
|  |                 binding.rvMessage.visibility = View.GONE | ||||||
|  |                 binding.llNoItems.visibility = View.VISIBLE | ||||||
|  |             } else { | ||||||
|  |                 binding.rvMessage.visibility = View.VISIBLE | ||||||
|  |                 binding.llNoItems.visibility = View.GONE | ||||||
|  |                 adapter.items.addAll(it) | ||||||
|  |                 adapter.notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.messageBoxLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             binding.tvSent.setBackgroundResource(R.drawable.bg_round_corner_16_7_transparent_777777) | ||||||
|  |             binding.tvSent.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             binding.tvReceive.setBackgroundResource( | ||||||
|  |                 R.drawable.bg_round_corner_16_7_transparent_777777 | ||||||
|  |             ) | ||||||
|  |             binding.tvReceive.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             binding.tvKeep.setBackgroundResource( | ||||||
|  |                 R.drawable.bg_round_corner_16_7_transparent_777777 | ||||||
|  |             ) | ||||||
|  |             binding.tvKeep.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             when (it) { | ||||||
|  |                 MessageBox.SENT -> { | ||||||
|  |                     binding.tvSent.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvSent.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.RECEIVE -> { | ||||||
|  |                     binding.tvReceive.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvReceive.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.KEEP -> { | ||||||
|  |                     binding.tvKeep.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvKeep.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 else -> { | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,105 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | 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.message.GetTextMessageResponse | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageBox | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageRepository | ||||||
|  |  | ||||||
|  | class TextMessageViewModel(private val repository: MessageRepository) : BaseViewModel() { | ||||||
|  |     private val _messageBoxLiveData = MutableLiveData(MessageBox.RECEIVE) | ||||||
|  |     val messageBoxLiveData: LiveData<MessageBox> | ||||||
|  |         get() = _messageBoxLiveData | ||||||
|  |  | ||||||
|  |     private val _getMessagesLiveData = | ||||||
|  |         MutableLiveData<List<GetTextMessageResponse.TextMessageItem>>() | ||||||
|  |     val getMessagesLiveData: LiveData<List<GetTextMessageResponse.TextMessageItem>> | ||||||
|  |         get() = _getMessagesLiveData | ||||||
|  |  | ||||||
|  |     private val _toastLiveData = MutableLiveData<String?>() | ||||||
|  |     val toastLiveData: LiveData<String?> | ||||||
|  |         get() = _toastLiveData | ||||||
|  |  | ||||||
|  |     var page = 1 | ||||||
|  |     var pageSize = 10 | ||||||
|  |     private var totalCount = 0 | ||||||
|  |  | ||||||
|  |     private var _isLoading = MutableLiveData(false) | ||||||
|  |     val isLoading: LiveData<Boolean> | ||||||
|  |         get() = _isLoading | ||||||
|  |  | ||||||
|  |     fun selectMessageBox(messageBox: MessageBox) { | ||||||
|  |         if (messageBox != _messageBoxLiveData.value!!) { | ||||||
|  |             page = 1 | ||||||
|  |             _messageBoxLiveData.postValue(messageBox) | ||||||
|  |             getMessages(messageBox) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getMessages(messageBox: MessageBox = _messageBoxLiveData.value!!) { | ||||||
|  |         if (!_isLoading.value!! && (page - 1 == 0 || totalCount > page * pageSize)) { | ||||||
|  |             _isLoading.postValue(true) | ||||||
|  |  | ||||||
|  |             val messageBoxObservable = when (messageBox) { | ||||||
|  |                 MessageBox.SENT -> { | ||||||
|  |                     repository.getSentTextMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.RECEIVE -> { | ||||||
|  |                     repository.getReceivedTextMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.KEEP -> { | ||||||
|  |                     repository.getKeepTextMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 messageBoxObservable | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 totalCount = it.data.totalCount | ||||||
|  |                                 _getMessagesLiveData.postValue(it.data.items) | ||||||
|  |  | ||||||
|  |                                 page += 1 | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.postValue(it.message) | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData.postValue( | ||||||
|  |                                         "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                     ) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             _isLoading.postValue(false) | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |                             _isLoading.postValue(false) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,117 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.core.content.IntentCompat | ||||||
|  | import com.jakewharton.rxbinding4.widget.textChanges | ||||||
|  | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers | ||||||
|  | import io.reactivex.rxjava3.schedulers.Schedulers | ||||||
|  | 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.databinding.ActivityTextMessageWriteBinding | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
|  | import kr.co.vividnext.sodalive.message.SelectMessageRecipientActivity | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  |  | ||||||
|  | class TextMessageWriteActivity : BaseActivity<ActivityTextMessageWriteBinding>( | ||||||
|  |     ActivityTextMessageWriteBinding::inflate | ||||||
|  | ) { | ||||||
|  |     private val viewModel: TextMessageWriteViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var loadingDialog: LoadingDialog | ||||||
|  |     private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |  | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         activityResultLauncher = registerForActivityResult( | ||||||
|  |             ActivityResultContracts.StartActivityForResult() | ||||||
|  |         ) { | ||||||
|  |             if (it.resultCode == Activity.RESULT_OK) { | ||||||
|  |                 if (it.data != null) { | ||||||
|  |                     val recipient = IntentCompat.getParcelableExtra( | ||||||
|  |                         it.data!!, | ||||||
|  |                         Constants.EXTRA_SELECT_RECIPIENT, | ||||||
|  |                         GetRoomDetailUser::class.java | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if (recipient != null) { | ||||||
|  |                         binding.tvRecipientNickname.text = recipient.nickname | ||||||
|  |                         viewModel.recipientId = recipient.id | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         bindData() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     override fun setupView() { | ||||||
|  |         loadingDialog = LoadingDialog(this, layoutInflater) | ||||||
|  |  | ||||||
|  |         val replySenderNickname = intent.getStringExtra(Constants.EXTRA_NICKNAME) | ||||||
|  |         val replySenderId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0) | ||||||
|  |  | ||||||
|  |         if (replySenderId > 0 && replySenderNickname != null) { | ||||||
|  |             binding.ivSelectRecipient.visibility = View.GONE | ||||||
|  |             binding.tvRecipientNickname.text = replySenderNickname | ||||||
|  |             viewModel.recipientId = replySenderId | ||||||
|  |  | ||||||
|  |             binding.tvTitle.text = "메시지 보내기" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvCancel.setOnClickListener { finish() } | ||||||
|  |  | ||||||
|  |         binding.tvSend.setOnClickListener { | ||||||
|  |             viewModel.write { | ||||||
|  |                 Toast.makeText( | ||||||
|  |                     applicationContext, | ||||||
|  |                     "메시지 전송이 완료되었습니다.", | ||||||
|  |                     Toast.LENGTH_LONG | ||||||
|  |                 ).show() | ||||||
|  |  | ||||||
|  |                 setResult(RESULT_OK) | ||||||
|  |                 finish() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivSelectRecipient.setOnClickListener { | ||||||
|  |             val intent = Intent(applicationContext, SelectMessageRecipientActivity::class.java) | ||||||
|  |             activityResultLauncher.launch(intent) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun bindData() { | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             binding.etMessage.textChanges().skip(1) | ||||||
|  |                 .debounce(500, TimeUnit.MILLISECONDS) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .subscribe { | ||||||
|  |                     viewModel.textMessage = it.toString() | ||||||
|  |                 } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         viewModel.toastLiveData.observe(this) { | ||||||
|  |             it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.isLoading.observe(this) { | ||||||
|  |             if (it) { | ||||||
|  |                 loadingDialog.show(screenWidth) | ||||||
|  |             } else { | ||||||
|  |                 loadingDialog.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.text | ||||||
|  |  | ||||||
|  | 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.message.MessageRepository | ||||||
|  | import kr.co.vividnext.sodalive.message.SendTextMessageRequest | ||||||
|  |  | ||||||
|  | class TextMessageWriteViewModel(private val repository: MessageRepository) : BaseViewModel() { | ||||||
|  |     var textMessage = "" | ||||||
|  |     var recipientId: Long = 0 | ||||||
|  |  | ||||||
|  |     private val _toastLiveData = MutableLiveData<String?>() | ||||||
|  |     val toastLiveData: LiveData<String?> | ||||||
|  |         get() = _toastLiveData | ||||||
|  |  | ||||||
|  |     private var _isLoading = MutableLiveData(false) | ||||||
|  |     val isLoading: LiveData<Boolean> | ||||||
|  |         get() = _isLoading | ||||||
|  |  | ||||||
|  |     fun write(onSuccess: () -> Unit) { | ||||||
|  |         if (recipientId <= 0) { | ||||||
|  |             _toastLiveData.postValue("받는 사람을 선택해 주세요.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (textMessage.isBlank()) { | ||||||
|  |             _toastLiveData.postValue("메시지를 입력하세요.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val request = SendTextMessageRequest(recipientId = recipientId, textMessage = textMessage) | ||||||
|  |         _isLoading.value = true | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             repository.sendTextMessage( | ||||||
|  |                 request = request, | ||||||
|  |                 token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |             ) | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                     { | ||||||
|  |                         _isLoading.value = false | ||||||
|  |                         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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |                         _isLoading.postValue(false) | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,154 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.voice | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.SeekBar | ||||||
|  | import android.widget.TextView | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import coil.load | ||||||
|  | import coil.transform.RoundedCornersTransformation | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.common.SharedPreferenceManager | ||||||
|  | import kr.co.vividnext.sodalive.databinding.ItemVoiceMessageBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import kr.co.vividnext.sodalive.message.GetVoiceMessageResponse | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageBox | ||||||
|  |  | ||||||
|  | class VoiceMessageAdapter( | ||||||
|  |     private val onPlay: (GetVoiceMessageResponse.VoiceMessageItem) -> Int, | ||||||
|  |     private val onStop: () -> Unit, | ||||||
|  |     private val onStartSeekbar: (SeekBar, TextView) -> Unit, | ||||||
|  |     private val onItemDelete: (Long) -> Unit, | ||||||
|  |     private val onItemKeep: (Long, Boolean) -> Unit, | ||||||
|  |     private val onItemReply: (Long, String, String) -> Unit | ||||||
|  | ) : RecyclerView.Adapter<VoiceMessageAdapter.ViewHolder>() { | ||||||
|  |  | ||||||
|  |     private var openPlayerItemPosition = -1 | ||||||
|  |     private var isPlaying = false | ||||||
|  |  | ||||||
|  |     private var messageBox = MessageBox.RECEIVE | ||||||
|  |  | ||||||
|  |     fun setMessageBox(messageBox: MessageBox) { | ||||||
|  |         this.messageBox = messageBox | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     inner class ViewHolder( | ||||||
|  |         private val binding: ItemVoiceMessageBinding | ||||||
|  |     ) : RecyclerView.ViewHolder(binding.root) { | ||||||
|  |         @SuppressLint("NotifyDataSetChanged") | ||||||
|  |         fun bind(item: GetVoiceMessageResponse.VoiceMessageItem, position: Int) { | ||||||
|  |             if (openPlayerItemPosition == position) { | ||||||
|  |                 binding.llPlayer.visibility = View.VISIBLE | ||||||
|  |                 if (isPlaying) { | ||||||
|  |                     onStartSeekbar(binding.seekbar, binding.tvTotalDuration) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.llPlayer.visibility = View.GONE | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (isPlaying) { | ||||||
|  |                 binding.ivPlayOrStop.setImageResource(R.drawable.btn_bar_stop) | ||||||
|  |             } else { | ||||||
|  |                 binding.ivPlayOrStop.setImageResource(R.drawable.btn_bar_play) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (SharedPreferenceManager.nickname == item.recipientNickname) { | ||||||
|  |                 binding.ivProfile.load(item.senderProfileImageUrl) { | ||||||
|  |                     crossfade(true) | ||||||
|  |                     placeholder(R.drawable.bg_placeholder) | ||||||
|  |                     transformations(RoundedCornersTransformation(23.4f.dpToPx())) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 binding.tvNickname.text = item.senderNickname | ||||||
|  |             } else { | ||||||
|  |                 binding.ivProfile.load(item.recipientProfileImageUrl) { | ||||||
|  |                     crossfade(true) | ||||||
|  |                     placeholder(R.drawable.bg_placeholder) | ||||||
|  |                     transformations(RoundedCornersTransformation(23.4f.dpToPx())) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 binding.tvNickname.text = item.recipientNickname | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.tvDate.text = item.date | ||||||
|  |             binding.root.setOnClickListener { | ||||||
|  |                 if (isPlaying) { | ||||||
|  |                     onStop() | ||||||
|  |                     isPlaying = false | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 openPlayerItemPosition = if ( | ||||||
|  |                     openPlayerItemPosition == position && | ||||||
|  |                     binding.llPlayer.visibility == View.VISIBLE | ||||||
|  |                 ) { | ||||||
|  |                     -1 | ||||||
|  |                 } else { | ||||||
|  |                     position | ||||||
|  |                 } | ||||||
|  |                 notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.llPlayer.setOnClickListener {} | ||||||
|  |             binding.seekbar.isEnabled = false | ||||||
|  |  | ||||||
|  |             binding.ivPlayOrStop.setOnClickListener { | ||||||
|  |                 if (isPlaying) { | ||||||
|  |                     isPlaying = false | ||||||
|  |                     onStop() | ||||||
|  |                     binding.ivPlayOrStop.setImageResource(R.drawable.btn_bar_play) | ||||||
|  |                 } else { | ||||||
|  |                     val totalDuration = onPlay(item).toLong() | ||||||
|  |                     if (totalDuration > 0) { | ||||||
|  |                         isPlaying = true | ||||||
|  |                         onStartSeekbar(binding.seekbar, binding.tvTotalDuration) | ||||||
|  |                         binding.ivPlayOrStop.setImageResource(R.drawable.btn_bar_stop) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (messageBox == MessageBox.RECEIVE) { | ||||||
|  |                 binding.ivKeep.visibility = View.VISIBLE | ||||||
|  |                 binding.ivReply.visibility = View.VISIBLE | ||||||
|  |                 binding.ivDelete.visibility = View.GONE | ||||||
|  |  | ||||||
|  |                 binding.ivKeep.setOnClickListener { onItemKeep(item.messageId, item.isKept) } | ||||||
|  |                 binding.ivReply.setOnClickListener { | ||||||
|  |                     onItemReply( | ||||||
|  |                         item.senderId, | ||||||
|  |                         item.senderNickname, | ||||||
|  |                         item.senderProfileImageUrl | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 binding.ivKeep.visibility = View.GONE | ||||||
|  |                 binding.ivReply.visibility = View.GONE | ||||||
|  |                 binding.ivDelete.visibility = View.VISIBLE | ||||||
|  |  | ||||||
|  |                 binding.ivDelete.setOnClickListener { onItemDelete(item.messageId) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val items = mutableListOf<GetVoiceMessageResponse.VoiceMessageItem>() | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( | ||||||
|  |         ItemVoiceMessageBinding.inflate( | ||||||
|  |             LayoutInflater.from(parent.context), | ||||||
|  |             parent, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         holder.bind(items[position], position) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount() = items.size | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     fun voicePlayComplete() { | ||||||
|  |         isPlaying = false | ||||||
|  |         notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,355 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.voice | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.graphics.Rect | ||||||
|  | import android.media.MediaPlayer | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.os.Handler | ||||||
|  | import android.os.Looper | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.SeekBar | ||||||
|  | import android.widget.TextView | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import com.orhanobut.logger.Logger | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.base.BaseFragment | ||||||
|  | import kr.co.vividnext.sodalive.common.LoadingDialog | ||||||
|  | import kr.co.vividnext.sodalive.databinding.FragmentVoiceMessageBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageBox | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  | import java.util.Timer | ||||||
|  | import java.util.TimerTask | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
|  |  | ||||||
|  | class VoiceMessageFragment : BaseFragment<FragmentVoiceMessageBinding>( | ||||||
|  |     FragmentVoiceMessageBinding::inflate | ||||||
|  | ) { | ||||||
|  |     private val viewModel: VoiceMessageViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var adapter: VoiceMessageAdapter | ||||||
|  |  | ||||||
|  |     private lateinit var loadingDialog: LoadingDialog | ||||||
|  |  | ||||||
|  |     private var mediaPlayer: MediaPlayer? = null | ||||||
|  |  | ||||||
|  |     private val handler = Handler(Looper.getMainLooper()) | ||||||
|  |     private var timerTask: TimerTask? = null | ||||||
|  |     private var timer: Timer? = null | ||||||
|  |  | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         if (mediaPlayer != null) { | ||||||
|  |             mediaPlayer!!.release() | ||||||
|  |             mediaPlayer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         super.onDestroy() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         setupView() | ||||||
|  |         bindData() | ||||||
|  |  | ||||||
|  |         viewModel.getMessages() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setupView() { | ||||||
|  |         loadingDialog = LoadingDialog(requireActivity(), layoutInflater) | ||||||
|  |  | ||||||
|  |         binding.tvSent.setOnClickListener { | ||||||
|  |             adapter.setMessageBox(MessageBox.SENT) | ||||||
|  |             viewModel.selectMessageBox( | ||||||
|  |                 MessageBox.SENT | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvReceive.setOnClickListener { | ||||||
|  |             adapter.setMessageBox(MessageBox.RECEIVE) | ||||||
|  |             viewModel.selectMessageBox( | ||||||
|  |                 MessageBox.RECEIVE | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvKeep.setOnClickListener { | ||||||
|  |             adapter.setMessageBox(MessageBox.KEEP) | ||||||
|  |             viewModel.selectMessageBox( | ||||||
|  |                 MessageBox.KEEP | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivWrite.setOnClickListener { | ||||||
|  |             stopVoiceMessage() | ||||||
|  |             val voiceWriteFragment = VoiceMessageWriteFragment( | ||||||
|  |                 onSendSuccess = { | ||||||
|  |                     viewModel.page = 1 | ||||||
|  |                     viewModel.getMessages() | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |             voiceWriteFragment.show(childFragmentManager, voiceWriteFragment.tag) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val recyclerView = binding.rvMessage | ||||||
|  |         adapter = VoiceMessageAdapter( | ||||||
|  |             onPlay = { playVoiceMessage(it.voiceMessageUrl) }, | ||||||
|  |             onStop = { stopVoiceMessage() }, | ||||||
|  |             onStartSeekbar = { seekbar, textView -> | ||||||
|  |                 startSeekbar(seekbar, textView) | ||||||
|  |             }, | ||||||
|  |             onItemDelete = { | ||||||
|  |                 viewModel.deleteMessage(it) { | ||||||
|  |                     Toast.makeText( | ||||||
|  |                         requireContext(), | ||||||
|  |                         "메시지가 삭제되었습니다.", | ||||||
|  |                         Toast.LENGTH_LONG | ||||||
|  |                     ).show() | ||||||
|  |  | ||||||
|  |                     viewModel.page = 1 | ||||||
|  |                     viewModel.getMessages() | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             onItemKeep = { messageId, isKept -> | ||||||
|  |                 if (isKept) { | ||||||
|  |                     Toast.makeText( | ||||||
|  |                         requireContext(), | ||||||
|  |                         "이미 보관된 메시지 입니다.", | ||||||
|  |                         Toast.LENGTH_LONG | ||||||
|  |                     ).show() | ||||||
|  |                     return@VoiceMessageAdapter | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 viewModel.keepVoiceMessage(messageId) | ||||||
|  |             }, | ||||||
|  |             onItemReply = { senderId, senderNickname, senderProfileUrl -> | ||||||
|  |                 stopVoiceMessage() | ||||||
|  |                 val voiceWriteFragment = VoiceMessageWriteFragment( | ||||||
|  |                     onSendSuccess = { | ||||||
|  |                         viewModel.page = 1 | ||||||
|  |                         viewModel.getMessages() | ||||||
|  |                     }, | ||||||
|  |                     senderId = senderId, | ||||||
|  |                     senderNickname = senderNickname, | ||||||
|  |                     senderProfileUrl = senderProfileUrl | ||||||
|  |                 ) | ||||||
|  |                 voiceWriteFragment.show(childFragmentManager, voiceWriteFragment.tag) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         recyclerView.layoutManager = LinearLayoutManager( | ||||||
|  |             requireContext(), | ||||||
|  |             LinearLayoutManager.VERTICAL, | ||||||
|  |             false | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         recyclerView.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() | ||||||
|  |                 outRect.top = 13.3f.dpToPx().toInt() | ||||||
|  |                 outRect.bottom = 13.3f.dpToPx().toInt() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         recyclerView.adapter = adapter | ||||||
|  |  | ||||||
|  |         recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |  | ||||||
|  |                 val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! | ||||||
|  |                     .findLastCompletelyVisibleItemPosition() | ||||||
|  |                 val itemTotalCount = recyclerView.adapter!!.itemCount - 1 | ||||||
|  |  | ||||||
|  |                 // 스크롤이 끝에 도달했는지 확인 | ||||||
|  |                 if (!recyclerView.canScrollVertically(1) && | ||||||
|  |                     lastVisibleItemPosition == itemTotalCount | ||||||
|  |                 ) { | ||||||
|  |                     viewModel.getMessages(viewModel.messageBoxLiveData.value!!) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun startSeekbar(seekbar: SeekBar, textView: TextView) { | ||||||
|  |         if (timer != null) { | ||||||
|  |             timer!!.cancel() | ||||||
|  |             timerTask = null | ||||||
|  |             timer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mediaPlayer != null) { | ||||||
|  |             val duration = mediaPlayer?.duration!! | ||||||
|  |             seekbar.max = duration | ||||||
|  |             var seconds = TimeUnit.MILLISECONDS.toSeconds(duration.toLong()) | ||||||
|  |             val minutes = seconds / 60 | ||||||
|  |             seconds %= 60 | ||||||
|  |             textView.text = String.format("%02d:%02d", minutes, seconds) | ||||||
|  |  | ||||||
|  |             timerTask = object : TimerTask() { | ||||||
|  |                 override fun run() { | ||||||
|  |                     handler.post { | ||||||
|  |                         seekbar.progress = mediaPlayer?.currentPosition ?: 0 | ||||||
|  |                         Logger.e("test") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             timer = Timer() | ||||||
|  |             timer!!.scheduleAtFixedRate(timerTask, 0, 100) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun playVoiceMessage(voiceMessageUrl: String): Int { | ||||||
|  |         if (mediaPlayer == null) { | ||||||
|  |             mediaPlayer = MediaPlayer() | ||||||
|  |             mediaPlayer!!.reset() | ||||||
|  |  | ||||||
|  |             mediaPlayer!!.setOnCompletionListener { | ||||||
|  |                 stopVoiceMessage() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             mediaPlayer!!.setOnPreparedListener { | ||||||
|  |                 it.start() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 mediaPlayer!!.setDataSource(voiceMessageUrl) | ||||||
|  |                 mediaPlayer!!.prepare() | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show() | ||||||
|  |                 return 0 | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return mediaPlayer!!.duration | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return 0 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun stopVoiceMessage() { | ||||||
|  |         if (timer != null) { | ||||||
|  |             timer!!.cancel() | ||||||
|  |             timerTask = null | ||||||
|  |             timer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mediaPlayer != null) { | ||||||
|  |             adapter.voicePlayComplete() | ||||||
|  |             mediaPlayer!!.release() | ||||||
|  |             mediaPlayer = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("NotifyDataSetChanged") | ||||||
|  |     private fun bindData() { | ||||||
|  |         viewModel.toastLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.isLoading.observe(viewLifecycleOwner) { | ||||||
|  |             if (it) { | ||||||
|  |                 loadingDialog.show(screenWidth, "음성 메시지를 불러오고 있습니다.") | ||||||
|  |             } else { | ||||||
|  |                 loadingDialog.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.getMessagesLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             if (viewModel.page - 1 == 1) { | ||||||
|  |                 adapter.items.clear() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (adapter.items.size == 0 && it.isEmpty()) { | ||||||
|  |                 binding.rvMessage.visibility = View.GONE | ||||||
|  |                 binding.llNoItems.visibility = View.VISIBLE | ||||||
|  |             } else { | ||||||
|  |                 binding.rvMessage.visibility = View.VISIBLE | ||||||
|  |                 binding.llNoItems.visibility = View.GONE | ||||||
|  |                 adapter.items.addAll(it) | ||||||
|  |                 adapter.notifyDataSetChanged() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.messageBoxLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             binding.tvSent.setBackgroundResource(R.drawable.bg_round_corner_16_7_transparent_777777) | ||||||
|  |             binding.tvSent.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             binding.tvReceive.setBackgroundResource( | ||||||
|  |                 R.drawable.bg_round_corner_16_7_transparent_777777 | ||||||
|  |             ) | ||||||
|  |             binding.tvReceive.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             binding.tvKeep.setBackgroundResource( | ||||||
|  |                 R.drawable.bg_round_corner_16_7_transparent_777777 | ||||||
|  |             ) | ||||||
|  |             binding.tvKeep.setTextColor( | ||||||
|  |                 ContextCompat.getColor( | ||||||
|  |                     requireContext(), | ||||||
|  |                     R.color.color_777777 | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             when (it) { | ||||||
|  |                 MessageBox.SENT -> { | ||||||
|  |                     binding.tvSent.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvSent.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.RECEIVE -> { | ||||||
|  |                     binding.tvReceive.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvReceive.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.KEEP -> { | ||||||
|  |                     binding.tvKeep.setBackgroundResource( | ||||||
|  |                         R.drawable.bg_round_corner_16_7_transparent_9970ff | ||||||
|  |                     ) | ||||||
|  |                     binding.tvKeep.setTextColor( | ||||||
|  |                         ContextCompat.getColor( | ||||||
|  |                             requireContext(), | ||||||
|  |                             R.color.color_9970ff | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 else -> { | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,178 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.voice | ||||||
|  |  | ||||||
|  | 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.message.GetVoiceMessageResponse | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageBox | ||||||
|  | import kr.co.vividnext.sodalive.message.MessageRepository | ||||||
|  |  | ||||||
|  | class VoiceMessageViewModel(private val repository: MessageRepository) : BaseViewModel() { | ||||||
|  |     private val _messageBoxLiveData = MutableLiveData(MessageBox.RECEIVE) | ||||||
|  |     val messageBoxLiveData: LiveData<MessageBox> | ||||||
|  |         get() = _messageBoxLiveData | ||||||
|  |  | ||||||
|  |     private val _getMessagesLiveData = | ||||||
|  |         MutableLiveData<List<GetVoiceMessageResponse.VoiceMessageItem>>() | ||||||
|  |     val getMessagesLiveData: LiveData<List<GetVoiceMessageResponse.VoiceMessageItem>> | ||||||
|  |         get() = _getMessagesLiveData | ||||||
|  |  | ||||||
|  |     private val _toastLiveData = MutableLiveData<String?>() | ||||||
|  |     val toastLiveData: LiveData<String?> | ||||||
|  |         get() = _toastLiveData | ||||||
|  |  | ||||||
|  |     var page = 1 | ||||||
|  |     var pageSize = 10 | ||||||
|  |     private var totalCount = 0 | ||||||
|  |  | ||||||
|  |     private var _isLoading = MutableLiveData(false) | ||||||
|  |     val isLoading: LiveData<Boolean> | ||||||
|  |         get() = _isLoading | ||||||
|  |  | ||||||
|  |     fun selectMessageBox(messageBox: MessageBox) { | ||||||
|  |         if (messageBox != _messageBoxLiveData.value!!) { | ||||||
|  |             page = 1 | ||||||
|  |             _messageBoxLiveData.postValue(messageBox) | ||||||
|  |             getMessages(messageBox) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getMessages(messageBox: MessageBox = _messageBoxLiveData.value!!) { | ||||||
|  |         if (!_isLoading.value!! && (page - 1 == 0 || totalCount > page * pageSize)) { | ||||||
|  |             _isLoading.postValue(true) | ||||||
|  |             val messageBoxObservable = when (messageBox) { | ||||||
|  |                 MessageBox.SENT -> { | ||||||
|  |                     repository.getSentVoiceMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.RECEIVE -> { | ||||||
|  |                     repository.getReceivedVoiceMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 MessageBox.KEEP -> { | ||||||
|  |                     repository.getKeepVoiceMessage( | ||||||
|  |                         page - 1, | ||||||
|  |                         pageSize, | ||||||
|  |                         "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             compositeDisposable.add( | ||||||
|  |                 messageBoxObservable | ||||||
|  |                     .subscribeOn(Schedulers.io()) | ||||||
|  |                     .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                     .subscribe( | ||||||
|  |                         { | ||||||
|  |                             if (it.success && it.data != null) { | ||||||
|  |                                 totalCount = it.data.totalCount | ||||||
|  |                                 _getMessagesLiveData.postValue(it.data.items) | ||||||
|  |  | ||||||
|  |                                 page += 1 | ||||||
|  |                             } else { | ||||||
|  |                                 if (it.message != null) { | ||||||
|  |                                     _toastLiveData.postValue(it.message) | ||||||
|  |                                 } else { | ||||||
|  |                                     _toastLiveData.postValue( | ||||||
|  |                                         "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                     ) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             _isLoading.postValue(false) | ||||||
|  |                         }, | ||||||
|  |                         { | ||||||
|  |                             it.message?.let { message -> Logger.e(message) } | ||||||
|  |                             _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |                             _isLoading.postValue(false) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun deleteMessage(messageId: Long, onSuccess: () -> Unit) { | ||||||
|  |         if (messageId <= 0) { | ||||||
|  |             _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             repository.deleteMessage( | ||||||
|  |                 messageId = messageId, | ||||||
|  |                 token = "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 keepVoiceMessage(messageId: Long) { | ||||||
|  |         if (messageId <= 0) { | ||||||
|  |             _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         _isLoading.value = true | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             repository.keepVoiceMessage( | ||||||
|  |                 messageId = messageId, | ||||||
|  |                 token = "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |             ).subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                     { | ||||||
|  |                         _isLoading.value = false | ||||||
|  |                         if (it.success) { | ||||||
|  |                             _toastLiveData.postValue( | ||||||
|  |                                 "보관되었습니다." | ||||||
|  |                             ) | ||||||
|  |                         } else { | ||||||
|  |                             if (it.message != null) { | ||||||
|  |                                 _toastLiveData.postValue(it.message) | ||||||
|  |                             } else { | ||||||
|  |                                 _toastLiveData.postValue( | ||||||
|  |                                     "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         it.message?.let { message -> Logger.e(message) } | ||||||
|  |                         _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |                         _isLoading.value = false | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,431 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.voice | ||||||
|  |  | ||||||
|  | import android.Manifest | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.Activity | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.Intent | ||||||
|  | import android.media.MediaPlayer | ||||||
|  | import android.media.MediaRecorder | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.os.CountDownTimer | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.FrameLayout | ||||||
|  | import android.widget.Toast | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
|  | import androidx.core.content.IntentCompat | ||||||
|  | import androidx.core.content.res.ResourcesCompat | ||||||
|  | import coil.load | ||||||
|  | import coil.transform.RoundedCornersTransformation | ||||||
|  | import com.google.android.material.bottomsheet.BottomSheetBehavior | ||||||
|  | import com.google.android.material.bottomsheet.BottomSheetDialog | ||||||
|  | import com.google.android.material.bottomsheet.BottomSheetDialogFragment | ||||||
|  | import com.gun0912.tedpermission.PermissionListener | ||||||
|  | import com.gun0912.tedpermission.normal.TedPermission | ||||||
|  | import kr.co.vividnext.sodalive.R | ||||||
|  | import kr.co.vividnext.sodalive.common.Constants | ||||||
|  | import kr.co.vividnext.sodalive.common.LoadingDialog | ||||||
|  | import kr.co.vividnext.sodalive.databinding.FragmentVoiceMessageWriteBinding | ||||||
|  | import kr.co.vividnext.sodalive.extensions.dpToPx | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
|  | import kr.co.vividnext.sodalive.message.SelectMessageRecipientActivity | ||||||
|  | import org.koin.android.ext.android.inject | ||||||
|  | import java.io.File | ||||||
|  | import java.io.IOException | ||||||
|  |  | ||||||
|  | class VoiceMessageWriteFragment( | ||||||
|  |     private val onSendSuccess: () -> Unit, | ||||||
|  |     private val senderId: Long? = null, | ||||||
|  |     private val senderNickname: String? = null, | ||||||
|  |     private val senderProfileUrl: String? = null | ||||||
|  | ) : BottomSheetDialogFragment() { | ||||||
|  |  | ||||||
|  |     private val viewModel: VoiceMessageWriteViewModel by inject() | ||||||
|  |  | ||||||
|  |     private lateinit var loadingDialog: LoadingDialog | ||||||
|  |     private lateinit var binding: FragmentVoiceMessageWriteBinding | ||||||
|  |     private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> | ||||||
|  |  | ||||||
|  |     private var countDownTimer: CountDownTimer? = null | ||||||
|  |     private var mediaRecorder: MediaRecorder? = null | ||||||
|  |     private var mediaPlayer: MediaPlayer? = null | ||||||
|  |     private var fileNameMedia = "" | ||||||
|  |  | ||||||
|  |     private var second = -1 | ||||||
|  |     private var minute = 0 | ||||||
|  |     private var hour = 0 | ||||||
|  |  | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         activityResultLauncher = registerForActivityResult( | ||||||
|  |             ActivityResultContracts.StartActivityForResult() | ||||||
|  |         ) { | ||||||
|  |             if (it.resultCode == Activity.RESULT_OK) { | ||||||
|  |                 if (it.data != null) { | ||||||
|  |                     val recipient = IntentCompat.getParcelableExtra( | ||||||
|  |                         it.data!!, | ||||||
|  |                         Constants.EXTRA_SELECT_RECIPIENT, | ||||||
|  |                         GetRoomDetailUser::class.java | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if (recipient != null) { | ||||||
|  |                         setReceiver( | ||||||
|  |                             userId = recipient.id, | ||||||
|  |                             nickname = recipient.nickname, | ||||||
|  |                             profileUrl = recipient.profileImageUrl | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         TedPermission.create() | ||||||
|  |             .setPermissionListener(object : PermissionListener { | ||||||
|  |                 override fun onPermissionGranted() { | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onPermissionDenied(deniedPermissions: MutableList<String>?) { | ||||||
|  |                     dismiss() | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .setDeniedMessage("오디오 녹음 권한을 거부하시면 음성 속닥을 이용하실 수 없습니다.") | ||||||
|  |             .setPermissions(Manifest.permission.RECORD_AUDIO) | ||||||
|  |             .check() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setReceiver(userId: Long, nickname: String, profileUrl: String) { | ||||||
|  |         binding.ivProfile.load(profileUrl) { | ||||||
|  |             crossfade(true) | ||||||
|  |             placeholder(R.drawable.bg_placeholder) | ||||||
|  |             transformations(RoundedCornersTransformation(23.4f.dpToPx())) | ||||||
|  |         } | ||||||
|  |         binding.tvNickname.text = nickname | ||||||
|  |         binding.tvNickname.typeface = ResourcesCompat.getFont( | ||||||
|  |             requireContext(), | ||||||
|  |             R.font.gmarket_sans_bold | ||||||
|  |         ) | ||||||
|  |         binding.tvNickname.setTextColor( | ||||||
|  |             ContextCompat.getColor( | ||||||
|  |                 requireContext(), | ||||||
|  |                 R.color.color_eeeeee | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         binding.ivPlus.visibility = View.GONE | ||||||
|  |         viewModel.recipientId = userId | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||||||
|  |         val dialog = super.onCreateDialog(savedInstanceState) | ||||||
|  |  | ||||||
|  |         dialog.setOnShowListener { | ||||||
|  |             val d = it as BottomSheetDialog | ||||||
|  |             val bottomSheet = d.findViewById<FrameLayout>( | ||||||
|  |                 com.google.android.material.R.id.design_bottom_sheet | ||||||
|  |             ) | ||||||
|  |             if (bottomSheet != null) { | ||||||
|  |                 BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return dialog | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         binding = FragmentVoiceMessageWriteBinding.inflate(inflater, container, false) | ||||||
|  |         return binding.root | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         loadingDialog = LoadingDialog(requireActivity(), layoutInflater) | ||||||
|  |  | ||||||
|  |         bindData() | ||||||
|  |  | ||||||
|  |         binding.ivClose.setOnClickListener { dismiss() } | ||||||
|  |  | ||||||
|  |         if (senderId != null && senderProfileUrl != null && senderNickname != null) { | ||||||
|  |             setReceiver( | ||||||
|  |                 userId = senderId, | ||||||
|  |                 nickname = senderNickname, | ||||||
|  |                 profileUrl = senderProfileUrl | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             binding.rlSelectRecipient.setOnClickListener { | ||||||
|  |                 val intent = Intent(requireContext(), SelectMessageRecipientActivity::class.java) | ||||||
|  |                 activityResultLauncher.launch(intent) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivRecordStart.setOnClickListener { | ||||||
|  |             if (viewModel.recipientId <= 0) { | ||||||
|  |                 Toast.makeText( | ||||||
|  |                     requireContext(), | ||||||
|  |                     "받는 사람을 선택해 주세요.", | ||||||
|  |                     Toast.LENGTH_LONG | ||||||
|  |                 ).show() | ||||||
|  |  | ||||||
|  |                 return@setOnClickListener | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             fileNameMedia = | ||||||
|  |                 "${requireActivity().filesDir.path}/socdoc_${System.currentTimeMillis()}.mp3" | ||||||
|  |  | ||||||
|  |             val fileMedia = File(fileNameMedia) | ||||||
|  |             if (!fileMedia.exists()) { | ||||||
|  |                 try { | ||||||
|  |                     fileMedia.createNewFile() | ||||||
|  |  | ||||||
|  |                     startRecording() | ||||||
|  |                 } catch (e: IOException) { | ||||||
|  |                     Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show() | ||||||
|  |                     e.printStackTrace() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivRecordStop.setOnClickListener { | ||||||
|  |             stopRecording() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivRecordPlay.setOnClickListener { | ||||||
|  |             startPlaying() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.ivRecordPause.setOnClickListener { | ||||||
|  |             stopPlaying() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvDelete.setOnClickListener { | ||||||
|  |             if (fileNameMedia.isNotBlank()) { | ||||||
|  |                 val fileMedia = File(fileNameMedia) | ||||||
|  |                 if (fileMedia.exists()) { | ||||||
|  |                     fileMedia.delete() | ||||||
|  |                 } | ||||||
|  |                 fileNameMedia = "" | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.ivRecordStart.visibility = View.VISIBLE | ||||||
|  |             binding.llRetryOrSend.visibility = View.GONE | ||||||
|  |             binding.rlRecordPlay.visibility = View.GONE | ||||||
|  |             binding.soundVisualizer.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvRetryRecord.setOnClickListener { | ||||||
|  |             if (fileNameMedia.isNotBlank()) { | ||||||
|  |                 val fileMedia = File(fileNameMedia) | ||||||
|  |                 if (fileMedia.exists()) { | ||||||
|  |                     fileMedia.delete() | ||||||
|  |                 } | ||||||
|  |                 fileNameMedia = "" | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.ivRecordStart.visibility = View.VISIBLE | ||||||
|  |             binding.llRetryOrSend.visibility = View.GONE | ||||||
|  |             binding.rlRecordPlay.visibility = View.GONE | ||||||
|  |             binding.soundVisualizer.visibility = View.GONE | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvSendMessage.setOnClickListener { | ||||||
|  |             viewModel.write(File(fileNameMedia)) { | ||||||
|  |                 dismiss() | ||||||
|  |                 onSendSuccess() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun bindData() { | ||||||
|  |         viewModel.toastLiveData.observe(viewLifecycleOwner) { | ||||||
|  |             it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModel.isLoading.observe(this) { | ||||||
|  |             if (it) { | ||||||
|  |                 loadingDialog.show(resources.displayMetrics.widthPixels) | ||||||
|  |             } else { | ||||||
|  |                 loadingDialog.dismiss() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         if (mediaPlayer != null) { | ||||||
|  |             mediaPlayer!!.release() | ||||||
|  |             mediaPlayer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (mediaRecorder != null) { | ||||||
|  |             // stop recording and free up resources | ||||||
|  |             mediaRecorder!!.stop() | ||||||
|  |             mediaRecorder!!.reset() | ||||||
|  |             mediaRecorder!!.release() | ||||||
|  |  | ||||||
|  |             mediaRecorder = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (fileNameMedia.isNotBlank()) { | ||||||
|  |             val fileMedia = File(fileNameMedia) | ||||||
|  |             if (fileMedia.exists()) { | ||||||
|  |                 fileMedia.delete() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             fileNameMedia = "" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         super.onDestroy() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun startRecording() { | ||||||
|  |         if (mediaRecorder == null) { | ||||||
|  |             // safety check, don't start a new recording if one is already going | ||||||
|  |             mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||||
|  |                 MediaRecorder(requireContext()) | ||||||
|  |             } else { | ||||||
|  |                 MediaRecorder() | ||||||
|  |             } | ||||||
|  |             mediaRecorder!!.setAudioSource(MediaRecorder.AudioSource.MIC) | ||||||
|  |             mediaRecorder!!.setOutputFile(fileNameMedia) | ||||||
|  |             mediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) | ||||||
|  |             mediaRecorder!!.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 mediaRecorder!!.prepare() | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show() | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             mediaRecorder!!.start() | ||||||
|  |             binding.ivRecordStart.visibility = View.GONE | ||||||
|  |             binding.ivRecordStop.visibility = View.VISIBLE | ||||||
|  |  | ||||||
|  |             startCountDownTimer() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun stopRecording() { | ||||||
|  |         if (mediaRecorder != null) { | ||||||
|  |             // stop recording and free up resources | ||||||
|  |             mediaRecorder!!.stop() | ||||||
|  |             mediaRecorder!!.reset() | ||||||
|  |             mediaRecorder!!.release() | ||||||
|  |  | ||||||
|  |             mediaRecorder = null | ||||||
|  |  | ||||||
|  |             binding.ivRecordStop.visibility = View.GONE | ||||||
|  |             binding.rlRecordPlay.visibility = View.VISIBLE | ||||||
|  |             binding.llRetryOrSend.visibility = View.VISIBLE | ||||||
|  |  | ||||||
|  |             stopCountDownTimer() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun startPlaying() { | ||||||
|  |         if (mediaPlayer == null) { | ||||||
|  |             mediaPlayer = MediaPlayer() | ||||||
|  |             mediaPlayer!!.reset() | ||||||
|  |  | ||||||
|  |             mediaPlayer!!.setOnCompletionListener { | ||||||
|  |                 if (mediaPlayer != null) { | ||||||
|  |                     mediaPlayer!!.release() | ||||||
|  |                     mediaPlayer = null | ||||||
|  |                 } | ||||||
|  |                 binding.tvDelete.visibility = View.VISIBLE | ||||||
|  |                 binding.ivRecordPlay.visibility = View.VISIBLE | ||||||
|  |                 binding.llRetryOrSend.visibility = View.VISIBLE | ||||||
|  |                 binding.ivRecordPause.visibility = View.GONE | ||||||
|  |                 binding.soundVisualizer.visibility = View.GONE | ||||||
|  |  | ||||||
|  |                 stopCountDownTimer() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             mediaPlayer!!.setOnPreparedListener { | ||||||
|  |                 binding.soundVisualizer.visibility = View.VISIBLE | ||||||
|  |                 binding.soundVisualizer.setAudioSessionId(mediaPlayer!!.audioSessionId) | ||||||
|  |                 it.start() | ||||||
|  |  | ||||||
|  |                 startCountDownTimer() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 mediaPlayer!!.setDataSource(fileNameMedia) | ||||||
|  |                 mediaPlayer!!.prepare() | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 Toast.makeText(requireActivity(), R.string.retry, Toast.LENGTH_LONG).show() | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             binding.tvDelete.visibility = View.GONE | ||||||
|  |             binding.ivRecordPlay.visibility = View.GONE | ||||||
|  |             binding.llRetryOrSend.visibility = View.GONE | ||||||
|  |             binding.ivRecordPause.visibility = View.VISIBLE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun stopPlaying() { | ||||||
|  |         if (mediaPlayer != null) { | ||||||
|  |             mediaPlayer!!.release() | ||||||
|  |             mediaPlayer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvDelete.visibility = View.VISIBLE | ||||||
|  |         binding.ivRecordPlay.visibility = View.VISIBLE | ||||||
|  |         binding.llRetryOrSend.visibility = View.VISIBLE | ||||||
|  |         binding.ivRecordPause.visibility = View.GONE | ||||||
|  |         binding.soundVisualizer.visibility = View.GONE | ||||||
|  |  | ||||||
|  |         stopCountDownTimer() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun startCountDownTimer() { | ||||||
|  |         countDownTimer = object : CountDownTimer(Long.MAX_VALUE, 1000) { | ||||||
|  |             override fun onTick(p0: Long) { | ||||||
|  |                 second += 1 | ||||||
|  |                 binding.tvTimer.text = recordingTime() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onFinish() { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         countDownTimer!!.start() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun recordingTime(): String { | ||||||
|  |         if (second == 60) { | ||||||
|  |             minute += 1 | ||||||
|  |             second = 0 | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (minute == 60) { | ||||||
|  |             hour += 1 | ||||||
|  |             minute = 0 | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return String.format("%02d:%02d:%02d", hour, minute, second) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("SetTextI18n") | ||||||
|  |     private fun stopCountDownTimer() { | ||||||
|  |         if (countDownTimer != null) { | ||||||
|  |             countDownTimer!!.cancel() | ||||||
|  |             countDownTimer = null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         binding.tvTimer.text = "00:00:00" | ||||||
|  |         second = -1 | ||||||
|  |         minute = 0 | ||||||
|  |         hour = 0 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | package kr.co.vividnext.sodalive.message.voice | ||||||
|  |  | ||||||
|  | import androidx.lifecycle.LiveData | ||||||
|  | import androidx.lifecycle.MutableLiveData | ||||||
|  | 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.message.MessageRepository | ||||||
|  | import kr.co.vividnext.sodalive.message.SendVoiceMessageRequest | ||||||
|  | import okhttp3.MediaType.Companion.toMediaType | ||||||
|  | import okhttp3.MultipartBody | ||||||
|  | import okhttp3.RequestBody.Companion.asRequestBody | ||||||
|  | import okhttp3.RequestBody.Companion.toRequestBody | ||||||
|  | import java.io.File | ||||||
|  |  | ||||||
|  | class VoiceMessageWriteViewModel(private val repository: MessageRepository) : BaseViewModel() { | ||||||
|  |     var recipientId: Long = 0 | ||||||
|  |  | ||||||
|  |     private val _toastLiveData = MutableLiveData<String?>() | ||||||
|  |     val toastLiveData: LiveData<String?> | ||||||
|  |         get() = _toastLiveData | ||||||
|  |  | ||||||
|  |     private var _isLoading = MutableLiveData(false) | ||||||
|  |     val isLoading: LiveData<Boolean> | ||||||
|  |         get() = _isLoading | ||||||
|  |  | ||||||
|  |     fun write(file: File, onSuccess: () -> Unit) { | ||||||
|  |         if (recipientId <= 0) { | ||||||
|  |             _toastLiveData.postValue("받는 사람을 선택해 주세요.") | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val request = SendVoiceMessageRequest(recipientId) | ||||||
|  |         val requestJson = Gson().toJson(request) | ||||||
|  |  | ||||||
|  |         val recordedFile = MultipartBody.Part.createFormData( | ||||||
|  |             "voiceMessageFile", | ||||||
|  |             file.name, | ||||||
|  |             file.asRequestBody("audio/mpeg".toMediaType()) | ||||||
|  |         ) | ||||||
|  |         _isLoading.value = true | ||||||
|  |         compositeDisposable.add( | ||||||
|  |             repository.sendVoiceMessage( | ||||||
|  |                 recordedFile, | ||||||
|  |                 requestJson.toRequestBody("text/plain".toMediaType()), | ||||||
|  |                 "Bearer ${SharedPreferenceManager.token}" | ||||||
|  |             ) | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                     { | ||||||
|  |                         _isLoading.value = false | ||||||
|  |                         if (it.success) { | ||||||
|  |                             _toastLiveData.postValue("메시지 전송이 완료되었습니다.") | ||||||
|  |                             onSuccess() | ||||||
|  |                         } else { | ||||||
|  |                             if (it.message != null) { | ||||||
|  |                                 _toastLiveData.postValue(it.message) | ||||||
|  |                             } else { | ||||||
|  |                                 _toastLiveData.postValue( | ||||||
|  |                                     "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         it.message?.let { message -> Logger.e(message) } | ||||||
|  |                         _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") | ||||||
|  |                         _isLoading.postValue(false) | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.user | |||||||
| import io.reactivex.rxjava3.core.Single | import io.reactivex.rxjava3.core.Single | ||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
| import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest | import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
| import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest | import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest | ||||||
| import kr.co.vividnext.sodalive.mypage.MyPageResponse | import kr.co.vividnext.sodalive.mypage.MyPageResponse | ||||||
| import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse | import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse | ||||||
| @@ -81,4 +82,10 @@ interface UserApi { | |||||||
|         request: Any, |         request: Any, | ||||||
|         @Header("Authorization") authHeader: String |         @Header("Authorization") authHeader: String | ||||||
|     ): Single<ApiResponse<Any>> |     ): Single<ApiResponse<Any>> | ||||||
|  |  | ||||||
|  |     @GET("/member/search") | ||||||
|  |     fun searchUser( | ||||||
|  |         @Query("nickname") nickname: String, | ||||||
|  |         @Header("Authorization") authHeader: String | ||||||
|  |     ): Single<ApiResponse<List<GetRoomDetailUser>>> | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.user | |||||||
| import io.reactivex.rxjava3.core.Single | import io.reactivex.rxjava3.core.Single | ||||||
| import kr.co.vividnext.sodalive.common.ApiResponse | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
| import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest | import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest | ||||||
|  | import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser | ||||||
| import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest | import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest | ||||||
| import kr.co.vividnext.sodalive.mypage.MyPageResponse | import kr.co.vividnext.sodalive.mypage.MyPageResponse | ||||||
| import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest | import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest | ||||||
| @@ -62,4 +63,11 @@ class UserRepository(private val userApi: UserApi) { | |||||||
|         request = CreatorFollowRequestRequest(creatorId = creatorId), |         request = CreatorFollowRequestRequest(creatorId = creatorId), | ||||||
|         authHeader = token |         authHeader = token | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     fun searchUser( | ||||||
|  |         nickname: String, | ||||||
|  |         token: String | ||||||
|  |     ): Single<ApiResponse<List<GetRoomDetailUser>>> { | ||||||
|  |         return userApi.searchUser(nickname, authHeader = token) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/btn_bar_play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 513 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/btn_bar_stop.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 294 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/btn_plus_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_make_message.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 571 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_make_voice.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 742 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_mic_paint.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 694 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_record.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_record_pause.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_record_play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_record_stop.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/ic_save.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 443 B | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-xxhdpi/img_thumb_default.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										8
									
								
								app/src/main/res/drawable/bg_round_corner_10_1b1b1b.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <solid android:color="@color/color_1b1b1b" /> | ||||||
|  |     <corners android:radius="10dp" /> | ||||||
|  |     <stroke | ||||||
|  |         android:width="1dp" | ||||||
|  |         android:color="@color/color_1b1b1b" /> | ||||||
|  | </shape> | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <solid android:color="@android:color/transparent" /> | ||||||
|  |     <corners android:radius="16.7dp" /> | ||||||
|  |     <stroke | ||||||
|  |         android:width="1dp" | ||||||
|  |         android:color="@color/color_777777" /> | ||||||
|  | </shape> | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <solid android:color="@color/color_339970ff" /> | ||||||
|  |     <corners android:radius="6.7dp" /> | ||||||
|  |     <stroke | ||||||
|  |         android:width="1dp" | ||||||
|  |         android:color="@color/color_339970ff" /> | ||||||
|  | </shape> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_delete.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | <vector android:height="24dp" android:tint="#FFFFFF" | ||||||
|  |     android:viewportHeight="24" android:viewportWidth="24" | ||||||
|  |     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										25
									
								
								app/src/main/res/drawable/voice_message_player_seekbar.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <item android:id="@android:id/background"> | ||||||
|  |         <shape> | ||||||
|  |             <corners android:radius="6.7dp" /> | ||||||
|  |             <solid android:color="@color/color_4dd8d8d8" /> | ||||||
|  |         </shape> | ||||||
|  |     </item> | ||||||
|  |     <item android:id="@android:id/secondaryProgress"> | ||||||
|  |         <clip> | ||||||
|  |             <shape> | ||||||
|  |                 <corners android:radius="6.7dp" /> | ||||||
|  |                 <solid android:color="@color/color_4dd8d8d8" /> | ||||||
|  |             </shape> | ||||||
|  |         </clip> | ||||||
|  |     </item> | ||||||
|  |     <item android:id="@android:id/progress"> | ||||||
|  |         <clip> | ||||||
|  |             <shape> | ||||||
|  |                 <corners android:radius="6.7dp" /> | ||||||
|  |                 <solid android:color="@color/color_9970ff" /> | ||||||
|  |             </shape> | ||||||
|  |         </clip> | ||||||
|  |     </item> | ||||||
|  | </layer-list> | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:background="@color/black" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/toolbar" | ||||||
|  |         layout="@layout/detail_toolbar" /> | ||||||
|  |  | ||||||
|  |     <EditText | ||||||
|  |         android:id="@+id/et_search_nickname" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="50dp" | ||||||
|  |         android:layout_marginHorizontal="13.3dp" | ||||||
|  |         android:layout_marginTop="20dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_10_232323" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:gravity="center_vertical" | ||||||
|  |         android:hint="검색" | ||||||
|  |         android:importantForAutofill="no" | ||||||
|  |         android:inputType="textWebEditText" | ||||||
|  |         android:paddingHorizontal="13.3dp" | ||||||
|  |         android:textColor="@color/color_eeeeee" | ||||||
|  |         android:textColorHint="@color/color_eeeeee" | ||||||
|  |         android:textCursorDrawable="@drawable/edit_text_cursor" | ||||||
|  |         android:textSize="13.3sp" | ||||||
|  |         tools:ignore="LabelFor" /> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_recipient" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:layout_marginTop="20dp" /> | ||||||
|  | </LinearLayout> | ||||||
							
								
								
									
										125
									
								
								app/src/main/res/layout/activity_text_message_detail.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,125 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:background="@color/black"> | ||||||
|  |  | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/toolbar" | ||||||
|  |         layout="@layout/detail_toolbar" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_profile" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginHorizontal="13.3dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_10_1b1b1b" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:paddingVertical="12.7dp" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/toolbar"> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:id="@+id/iv_profile" | ||||||
|  |             android:layout_width="26.7dp" | ||||||
|  |             android:layout_height="26.7dp" | ||||||
|  |             android:layout_marginEnd="13.3dp" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             tools:src="@mipmap/ic_launcher" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_nickname" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="18.3sp" | ||||||
|  |             tools:text="이재형 대표님" /> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_date" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginTop="16.7dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:textColor="@color/color_bbbbbb" | ||||||
|  |         android:textSize="15sp" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="@+id/ll_profile" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/ll_profile" | ||||||
|  |         tools:text="2021년 7월 14일 수요일 12:00" /> | ||||||
|  |  | ||||||
|  |     <ScrollView | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:layout_marginTop="16.7dp" | ||||||
|  |         android:layout_marginBottom="30dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_10_222222" | ||||||
|  |         android:paddingHorizontal="26.7dp" | ||||||
|  |         android:paddingVertical="13.3dp" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@+id/ll_buttons" | ||||||
|  |         app:layout_constraintEnd_toEndOf="@+id/ll_profile" | ||||||
|  |         app:layout_constraintStart_toStartOf="@+id/ll_profile" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/tv_date"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_message" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="15sp" /> | ||||||
|  |     </ScrollView> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_buttons" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="48.7dp" | ||||||
|  |         android:layout_marginHorizontal="13.3dp" | ||||||
|  |         android:layout_marginBottom="26.7dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_reply" | ||||||
|  |             android:layout_width="0dp" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:layout_weight="1" | ||||||
|  |             android:background="@drawable/bg_round_corner_6_7_9970ff" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:text="답장" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="14.7sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_keep" | ||||||
|  |             android:layout_width="0dp" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:layout_marginHorizontal="6.7dp" | ||||||
|  |             android:layout_weight="1" | ||||||
|  |             android:background="@drawable/bg_round_corner_6_7_1f1734" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:text="보관" | ||||||
|  |             android:textColor="@color/color_9970ff" | ||||||
|  |             android:textSize="14.7sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_delete" | ||||||
|  |             android:layout_width="0dp" | ||||||
|  |             android:layout_height="match_parent" | ||||||
|  |             android:layout_weight="1" | ||||||
|  |             android:background="@drawable/bg_round_corner_6_7_1f1734" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:text="삭제" | ||||||
|  |             android:textColor="@color/color_9970ff" | ||||||
|  |             android:textSize="14.7sp" /> | ||||||
|  |     </LinearLayout> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										126
									
								
								app/src/main/res/layout/activity_text_message_write.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:background="@color/black" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |     <RelativeLayout | ||||||
|  |         android:id="@+id/toolbar" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="50dp"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_title" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_centerInParent="true" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:text="새로운 메시지" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="18.3sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_cancel" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_alignParentEnd="true" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:layout_marginEnd="13.3dp" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:text="취소" | ||||||
|  |             android:textColor="@color/color_9970ff" | ||||||
|  |             android:textSize="16.7sp" /> | ||||||
|  |  | ||||||
|  |     </RelativeLayout> | ||||||
|  |  | ||||||
|  |     <RelativeLayout | ||||||
|  |         android:id="@+id/rl_recipient" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="50dp" | ||||||
|  |         android:layout_below="@+id/toolbar"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_recipient_title" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:layout_marginStart="13.3dp" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:text="받는 사람" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="16.7sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_recipient_nickname" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:layout_marginStart="13.3dp" | ||||||
|  |             android:layout_toEndOf="@+id/tv_recipient_title" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="16.7sp" | ||||||
|  |             tools:ignore="RelativeOverlap" | ||||||
|  |             tools:text="재민" /> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:id="@+id/iv_select_recipient" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_alignParentEnd="true" | ||||||
|  |             android:layout_centerVertical="true" | ||||||
|  |             android:layout_marginEnd="13.3dp" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             android:src="@drawable/btn_plus_round" /> | ||||||
|  |  | ||||||
|  |         <View | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="1dp" | ||||||
|  |             android:layout_alignParentBottom="true" | ||||||
|  |             android:background="@color/color_909090" /> | ||||||
|  |  | ||||||
|  |     </RelativeLayout> | ||||||
|  |  | ||||||
|  |     <ScrollView | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_below="@+id/rl_recipient" | ||||||
|  |         android:layout_marginHorizontal="13.3dp" | ||||||
|  |         android:layout_marginTop="13.3dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_10_232323_9970ff"> | ||||||
|  |  | ||||||
|  |         <EditText | ||||||
|  |             android:id="@+id/et_message" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:background="@null" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:gravity="top" | ||||||
|  |             android:hint="내용을 입력해 주세요" | ||||||
|  |             android:importantForAutofill="no" | ||||||
|  |             android:inputType="textMultiLine" | ||||||
|  |             android:minHeight="239dp" | ||||||
|  |             android:padding="20dp" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textColorHint="@color/color_777777" | ||||||
|  |             android:textCursorDrawable="@drawable/edit_text_cursor" | ||||||
|  |             android:textSize="13.3sp" | ||||||
|  |             tools:ignore="LabelFor" /> | ||||||
|  |     </ScrollView> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_send" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="48.7dp" | ||||||
|  |         android:layout_alignParentBottom="true" | ||||||
|  |         android:layout_marginHorizontal="13.3dp" | ||||||
|  |         android:layout_marginBottom="13.3dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_6_7_9970ff" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:text="메시지 보내기" | ||||||
|  |         android:textColor="@color/color_eeeeee" | ||||||
|  |         android:textSize="14.7sp" /> | ||||||
|  | </RelativeLayout> | ||||||
| @@ -1,16 +1,40 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|     <TextView |     <TextView | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="50dp" | ||||||
|         app:layout_constraintBottom_toBottomOf="parent" |         android:background="@color/black" | ||||||
|         app:layout_constraintEnd_toEndOf="parent" |         android:fontFamily="@font/gmarket_sans_bold" | ||||||
|         app:layout_constraintStart_toStartOf="parent" |         android:gravity="center_vertical" | ||||||
|  |         android:paddingHorizontal="13.3dp" | ||||||
|         android:text="메시지" |         android:text="메시지" | ||||||
|         app:layout_constraintTop_toTopOf="parent" /> |         android:textColor="@color/color_eeeeee" | ||||||
|  |         android:textSize="18.3sp" /> | ||||||
|  |  | ||||||
| </androidx.constraintlayout.widget.ConstraintLayout> |     <com.google.android.material.tabs.TabLayout | ||||||
|  |         android:id="@+id/tabs" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="50dp" | ||||||
|  |         app:tabIndicatorColor="@color/color_9970ff" | ||||||
|  |         app:tabIndicatorFullWidth="true" | ||||||
|  |         app:tabIndicatorHeight="1.3dp" | ||||||
|  |         app:tabSelectedTextColor="@color/color_eeeeee" | ||||||
|  |         app:tabTextAppearance="@style/tabText" | ||||||
|  |         app:tabTextColor="@color/color_777777" /> | ||||||
|  |  | ||||||
|  |     <View | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="1dp" | ||||||
|  |         android:background="@color/color_88909090" /> | ||||||
|  |  | ||||||
|  |     <FrameLayout | ||||||
|  |         android:id="@+id/container" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" /> | ||||||
|  |  | ||||||
|  | </LinearLayout> | ||||||
|   | |||||||
							
								
								
									
										127
									
								
								app/src/main/res/layout/fragment_text_message.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_notice" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginHorizontal="10dp" | ||||||
|  |         android:layout_marginTop="20dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:text="※ 보관하지 않은 받은 메시지는 3일 후, 자동 삭제됩니다." | ||||||
|  |         android:textSize="12sp" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_filter" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginTop="20dp" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:orientation="horizontal" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/tv_notice"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_receive" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="받은 메시지" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_sent" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginHorizontal="6.7dp" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="보낸 메시지" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_keep" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="보관함" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |  | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_no_items" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:layout_margin="13.3dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_4_7_2b2635" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/ll_filter"> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             android:src="@drawable/ic_no_item" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:lineSpacingExtra="4sp" | ||||||
|  |             android:text="메시지가 없습니다.\n친구들과 소통해보세요!" | ||||||
|  |             android:textColor="@color/color_bbbbbb" | ||||||
|  |             android:textSize="10.7sp" | ||||||
|  |             tools:ignore="SmallSp" /> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_message" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/ll_filter" /> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_write" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginEnd="16.7dp" | ||||||
|  |         android:layout_marginBottom="80dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_33_3_9970ff" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         android:src="@drawable/ic_make_message" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" /> | ||||||
|  |  | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										126
									
								
								app/src/main/res/layout/fragment_voice_message.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_notice" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginHorizontal="10dp" | ||||||
|  |         android:layout_marginTop="20dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:text="※ 보관하지 않은 받은 메시지는 3일 후, 자동 삭제됩니다." | ||||||
|  |         android:textSize="12sp" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_filter" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginTop="20dp" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:orientation="horizontal" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/tv_notice"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_receive" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="받은 메시지" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_sent" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginHorizontal="6.7dp" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="보낸 메시지" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_keep" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:background="@drawable/bg_round_corner_16_7_transparent_777777" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:paddingHorizontal="25dp" | ||||||
|  |             android:paddingVertical="10.7dp" | ||||||
|  |             android:text="보관함" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" /> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_no_items" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:layout_margin="13.3dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_4_7_2b2635" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/ll_filter"> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             android:src="@drawable/ic_no_item" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:lineSpacingExtra="4sp" | ||||||
|  |             android:text="메시지가 없습니다.\n친구들과 소통해보세요!" | ||||||
|  |             android:textColor="@color/color_bbbbbb" | ||||||
|  |             android:textSize="10.7sp" | ||||||
|  |             tools:ignore="SmallSp" /> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/rv_message" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:clipToPadding="false" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/ll_filter" /> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_write" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginEnd="16.7dp" | ||||||
|  |         android:layout_marginBottom="80dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_33_3_9970ff" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         android:padding="13.3dp" | ||||||
|  |         android:src="@drawable/ic_make_voice" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" /> | ||||||
|  |  | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										218
									
								
								app/src/main/res/layout/fragment_voice_message_write.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,218 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     tools:background="@color/color_222222"> | ||||||
|  |  | ||||||
|  |     <androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |             android:paddingHorizontal="26.7dp" | ||||||
|  |             android:paddingTop="26.7dp" | ||||||
|  |             android:text="음성메시지" | ||||||
|  |             android:textColor="@color/white" | ||||||
|  |             android:textSize="18.3sp" | ||||||
|  |             app:layout_constraintStart_toStartOf="parent" | ||||||
|  |             app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |         <ImageView | ||||||
|  |             android:id="@+id/iv_close" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:contentDescription="@null" | ||||||
|  |             android:paddingHorizontal="26.7dp" | ||||||
|  |             android:paddingTop="26.7dp" | ||||||
|  |             android:src="@drawable/ic_close_white" | ||||||
|  |             app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |             app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:id="@+id/rl_select_recipient" | ||||||
|  |             android:layout_width="0dp" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginHorizontal="13.3dp" | ||||||
|  |             android:layout_marginTop="26.7dp" | ||||||
|  |             android:background="@drawable/bg_round_corner_6_7_339970ff" | ||||||
|  |             android:orientation="horizontal" | ||||||
|  |             android:padding="13.3dp" | ||||||
|  |             app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |             app:layout_constraintStart_toStartOf="parent" | ||||||
|  |             app:layout_constraintTop_toBottomOf="@+id/iv_close"> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_profile" | ||||||
|  |                 android:layout_width="46.7dp" | ||||||
|  |                 android:layout_height="46.7dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/img_thumb_default" /> | ||||||
|  |  | ||||||
|  |             <LinearLayout | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_gravity="center_vertical" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:layout_toStartOf="@+id/iv_plus" | ||||||
|  |                 android:layout_toEndOf="@+id/iv_profile" | ||||||
|  |                 android:orientation="vertical"> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                     android:text="TO." | ||||||
|  |                     android:textColor="@color/color_eeeeee" | ||||||
|  |                     android:textSize="13.3sp" /> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tv_nickname" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_marginTop="10dp" | ||||||
|  |                     android:fontFamily="@font/gmarket_sans_light" | ||||||
|  |                     android:text="받는 사람" | ||||||
|  |                     android:textColor="@color/color_bbbbbb" | ||||||
|  |                     android:textSize="16.7sp" /> | ||||||
|  |             </LinearLayout> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_plus" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/btn_plus_round" /> | ||||||
|  |         </RelativeLayout> | ||||||
|  |  | ||||||
|  |         <LinearLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginTop="81dp" | ||||||
|  |             android:gravity="center" | ||||||
|  |             android:orientation="vertical" | ||||||
|  |             app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |             app:layout_constraintStart_toStartOf="parent" | ||||||
|  |             app:layout_constraintTop_toBottomOf="@+id/rl_select_recipient"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_timer" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_light" | ||||||
|  |                 android:text="00:00:00" | ||||||
|  |                 android:textColor="@color/white" | ||||||
|  |                 android:textSize="33.3sp" /> | ||||||
|  |  | ||||||
|  |             <com.gauravk.audiovisualizer.visualizer.WaveVisualizer | ||||||
|  |                 android:id="@+id/sound_visualizer" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="120dp" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:visibility="gone" | ||||||
|  |                 app:avColor="@color/av_deep_orange" | ||||||
|  |                 app:avDensity="0.8" | ||||||
|  |                 app:avSpeed="normal" | ||||||
|  |                 app:avType="fill" /> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_record_start" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginVertical="52.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_record" /> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_record_stop" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginVertical="52.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_record_stop" | ||||||
|  |                 android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |             <RelativeLayout | ||||||
|  |                 android:id="@+id/rl_record_play" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:visibility="gone"> | ||||||
|  |  | ||||||
|  |                 <ImageView | ||||||
|  |                     android:id="@+id/iv_record_play" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_centerInParent="true" | ||||||
|  |                     android:layout_marginTop="90dp" | ||||||
|  |                     android:contentDescription="@null" | ||||||
|  |                     android:src="@drawable/ic_record_play" /> | ||||||
|  |  | ||||||
|  |                 <ImageView | ||||||
|  |                     android:id="@+id/iv_record_pause" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_centerInParent="true" | ||||||
|  |                     android:layout_marginTop="90dp" | ||||||
|  |                     android:contentDescription="@null" | ||||||
|  |                     android:src="@drawable/ic_record_pause" | ||||||
|  |                     android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tv_delete" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_centerVertical="true" | ||||||
|  |                     android:layout_marginStart="60dp" | ||||||
|  |                     android:layout_toEndOf="@+id/iv_record_play" | ||||||
|  |                     android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                     android:text="삭제" | ||||||
|  |                     android:textColor="@color/color_bbbbbb" | ||||||
|  |                     android:textSize="15.3sp" /> | ||||||
|  |  | ||||||
|  |             </RelativeLayout> | ||||||
|  |  | ||||||
|  |             <LinearLayout | ||||||
|  |                 android:id="@+id/ll_retry_or_send" | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginHorizontal="13.3dp" | ||||||
|  |                 android:layout_marginTop="26.7dp" | ||||||
|  |                 android:layout_marginBottom="13.3dp" | ||||||
|  |                 android:orientation="horizontal" | ||||||
|  |                 android:visibility="gone"> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tv_retry_record" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="50dp" | ||||||
|  |                     android:layout_weight="1" | ||||||
|  |                     android:background="@drawable/bg_round_corner_10_339970ff_9970ff" | ||||||
|  |                     android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="다시 녹음" | ||||||
|  |                     android:textColor="@color/color_9970ff" | ||||||
|  |                     android:textSize="18.3sp" /> | ||||||
|  |  | ||||||
|  |                 <TextView | ||||||
|  |                     android:id="@+id/tv_send_message" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="50dp" | ||||||
|  |                     android:layout_marginStart="13.3dp" | ||||||
|  |                     android:layout_weight="2" | ||||||
|  |                     android:background="@drawable/bg_round_corner_10_9970ff" | ||||||
|  |                     android:fontFamily="@font/gmarket_sans_bold" | ||||||
|  |                     android:gravity="center" | ||||||
|  |                     android:text="메시지 보내기" | ||||||
|  |                     android:textColor="@color/white" | ||||||
|  |                     android:textSize="18.3sp" /> | ||||||
|  |  | ||||||
|  |             </LinearLayout> | ||||||
|  |         </LinearLayout> | ||||||
|  |     </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  | </androidx.core.widget.NestedScrollView> | ||||||
							
								
								
									
										65
									
								
								app/src/main/res/layout/item_text_message.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:background="@color/black"> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_profile" | ||||||
|  |         android:layout_width="46.7dp" | ||||||
|  |         android:layout_height="46.7dp" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:layout_marginStart="13.3dp" | ||||||
|  |         android:layout_marginEnd="35.3dp" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@+id/tv_date" | ||||||
|  |         app:layout_constraintStart_toEndOf="@+id/iv_profile" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent"> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_nickname" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:ellipsize="end" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |             android:maxLines="1" | ||||||
|  |             android:textColor="@color/color_eeeeee" | ||||||
|  |             android:textSize="13.3sp" | ||||||
|  |             tools:text="dlksjfei" /> | ||||||
|  |  | ||||||
|  |         <TextView | ||||||
|  |             android:id="@+id/tv_message" | ||||||
|  |             android:layout_width="wrap_content" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:ellipsize="end" | ||||||
|  |             android:fontFamily="@font/gmarket_sans_light" | ||||||
|  |             android:maxLines="1" | ||||||
|  |             android:textColor="@color/color_777777" | ||||||
|  |             android:textSize="12sp" | ||||||
|  |             tools:text="마지막 메세지 내용. 네 감사합니다. 그럼 다음에 또 연락드릴께요~!" /> | ||||||
|  |     </LinearLayout> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_date" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_light" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:textColor="@color/color_525252" | ||||||
|  |         android:textSize="12sp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         tools:text="8월 04일" /> | ||||||
|  |  | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										142
									
								
								app/src/main/res/layout/item_voice_message.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,142 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:background="@color/black"> | ||||||
|  |  | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_profile" | ||||||
|  |         android:layout_width="46.7dp" | ||||||
|  |         android:layout_height="46.7dp" | ||||||
|  |         android:contentDescription="@null" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_nickname" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginStart="13.3dp" | ||||||
|  |         android:layout_marginEnd="35.3dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |         android:textColor="@color/color_eeeeee" | ||||||
|  |         android:textSize="13.3sp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="@+id/iv_profile" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@+id/tv_date" | ||||||
|  |         app:layout_constraintStart_toEndOf="@+id/iv_profile" | ||||||
|  |         app:layout_constraintTop_toTopOf="@+id/iv_profile" | ||||||
|  |         tools:text="dlksjfei" /> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/tv_date" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:fontFamily="@font/gmarket_sans_light" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:textColor="@color/color_525252" | ||||||
|  |         android:textSize="12sp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="@+id/iv_profile" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="@+id/iv_profile" | ||||||
|  |         tools:text="8월 04일" /> | ||||||
|  |  | ||||||
|  |     <LinearLayout | ||||||
|  |         android:id="@+id/ll_player" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginTop="10dp" | ||||||
|  |         android:background="@drawable/bg_round_corner_6_7_339970ff" | ||||||
|  |         android:gravity="center" | ||||||
|  |         android:orientation="vertical" | ||||||
|  |         android:paddingVertical="20dp" | ||||||
|  |         android:visibility="visible" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@+id/iv_profile"> | ||||||
|  |  | ||||||
|  |         <SeekBar | ||||||
|  |             android:id="@+id/seekbar" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:progressDrawable="@drawable/voice_message_player_seekbar" | ||||||
|  |             android:thumb="@null" /> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginTop="6.7dp"> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_marginStart="13.3dp" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:text="00:00" | ||||||
|  |                 android:textColor="@color/color_bbbbbb" | ||||||
|  |                 android:textSize="10.7sp" | ||||||
|  |                 tools:ignore="SmallSp" /> | ||||||
|  |  | ||||||
|  |             <TextView | ||||||
|  |                 android:id="@+id/tv_total_duration" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_marginEnd="13.3dp" | ||||||
|  |                 android:fontFamily="@font/gmarket_sans_medium" | ||||||
|  |                 android:text="00:00" | ||||||
|  |                 android:textColor="@color/color_bbbbbb" | ||||||
|  |                 android:textSize="10.7sp" | ||||||
|  |                 tools:ignore="SmallSp" /> | ||||||
|  |         </RelativeLayout> | ||||||
|  |  | ||||||
|  |         <RelativeLayout | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="wrap_content" | ||||||
|  |             android:layout_marginTop="24.3dp"> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_keep" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_alignParentStart="true" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_marginStart="13.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_save" | ||||||
|  |                 android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_play_or_stop" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_centerInParent="true" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/btn_bar_play" /> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_delete" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_marginEnd="13.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_delete" | ||||||
|  |                 android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |             <ImageView | ||||||
|  |                 android:id="@+id/iv_reply" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_alignParentEnd="true" | ||||||
|  |                 android:layout_centerVertical="true" | ||||||
|  |                 android:layout_marginEnd="13.3dp" | ||||||
|  |                 android:contentDescription="@null" | ||||||
|  |                 android:src="@drawable/ic_mic_paint" | ||||||
|  |                 android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |         </RelativeLayout> | ||||||
|  |     </LinearLayout> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| @@ -68,4 +68,5 @@ | |||||||
|     <color name="color_ffb600">#FFB600</color> |     <color name="color_ffb600">#FFB600</color> | ||||||
|     <color name="color_99000000">#99000000</color> |     <color name="color_99000000">#99000000</color> | ||||||
|     <color name="color_4c9970ff">#4C9970FF</color> |     <color name="color_4c9970ff">#4C9970FF</color> | ||||||
|  |     <color name="color_4dd8d8d8">#4DD8D8D8</color> | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ dependencyResolutionManagement { | |||||||
|     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) |     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) | ||||||
|     repositories { |     repositories { | ||||||
|         google() |         google() | ||||||
|  |         jcenter() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         maven { url 'https://jitpack.io' } |         maven { url 'https://jitpack.io' } | ||||||
|     } |     } | ||||||
|   | |||||||