메시지 페이지 추가
| @@ -139,4 +139,7 @@ dependencies { | ||||
|     // agora | ||||
|     implementation "io.agora.rtc:voice-sdk:4.1.0-1" | ||||
|     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.CreatorNoticeWriteActivity" /> | ||||
|         <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 | ||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" | ||||
|   | ||||
| @@ -17,10 +17,14 @@ object Constants { | ||||
|     const val EXTRA_TERMS = "extra_terms" | ||||
|     const val EXTRA_ROOM_ID = "extra_room_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_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_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_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.update.LiveRoomEditViewModel | ||||
| 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.auth.AuthApi | ||||
| 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(), LiveRecommendApi::class.java) } | ||||
|         single { ApiBuilder().build(get(), ExplorerApi::class.java) } | ||||
|         single { ApiBuilder().build(get(), MessageApi::class.java) } | ||||
|     } | ||||
|  | ||||
|     private val viewModelModule = module { | ||||
| @@ -116,6 +124,11 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | ||||
|         viewModel { ExplorerViewModel(get()) } | ||||
|         viewModel { UserProfileViewModel(get(), 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 { | ||||
| @@ -129,6 +142,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { | ||||
|         factory { LiveTagRepository(get()) } | ||||
|         factory { ReportRepository(get()) } | ||||
|         factory { ExplorerRepository(get()) } | ||||
|         factory { MessageRepository(get()) } | ||||
|     } | ||||
|  | ||||
|     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.explorer.profile.UserProfileActivity | ||||
| 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 java.util.concurrent.TimeUnit | ||||
|  | ||||
| @@ -36,7 +36,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>( | ||||
|     private lateinit var imm: InputMethodManager | ||||
|  | ||||
|     private val handler = Handler(Looper.getMainLooper()) | ||||
|     private lateinit var searchChannelAdapter: MessageSelectRecipientAdapter | ||||
|     private lateinit var searchChannelAdapter: SelectMessageRecipientAdapter | ||||
|  | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
| @@ -106,7 +106,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>( | ||||
|     } | ||||
|  | ||||
|     private fun setupSearchChannelView() { | ||||
|         searchChannelAdapter = MessageSelectRecipientAdapter { | ||||
|         searchChannelAdapter = SelectMessageRecipientAdapter { | ||||
|             hideKeyboard() | ||||
|             val intent = Intent(requireContext(), UserProfileActivity::class.java) | ||||
|             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.GetRecentRoomInfoResponse | ||||
| 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.GetLiveRoomDonationStatusResponse | ||||
| import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse | ||||
| @@ -182,4 +183,9 @@ interface LiveApi { | ||||
|         @Path("id") id: Long, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): 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, | ||||
|         token: String | ||||
|     ) = 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 | ||||
|  | ||||
| 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.databinding.FragmentMessageBinding | ||||
| import kr.co.vividnext.sodalive.message.text.TextMessageFragment | ||||
| import kr.co.vividnext.sodalive.message.voice.VoiceMessageFragment | ||||
|  | ||||
| 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.live.room.detail.GetRoomDetailUser | ||||
| 
 | ||||
| class MessageSelectRecipientAdapter( | ||||
| class SelectMessageRecipientAdapter( | ||||
|     private val onClickItem: (GetRoomDetailUser) -> Unit | ||||
| ) : RecyclerView.Adapter<MessageSelectRecipientAdapter.ViewHolder>() { | ||||
| ) : RecyclerView.Adapter<SelectMessageRecipientAdapter.ViewHolder>() { | ||||
|     inner class ViewHolder( | ||||
|         private val binding: ItemSelectRecipientBinding | ||||
|     ) : 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 kr.co.vividnext.sodalive.common.ApiResponse | ||||
| 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.mypage.MyPageResponse | ||||
| import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse | ||||
| @@ -81,4 +82,10 @@ interface UserApi { | ||||
|         request: Any, | ||||
|         @Header("Authorization") authHeader: String | ||||
|     ): 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 kr.co.vividnext.sodalive.common.ApiResponse | ||||
| 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.mypage.MyPageResponse | ||||
| import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest | ||||
| @@ -62,4 +63,11 @@ class UserRepository(private val userApi: UserApi) { | ||||
|         request = CreatorFollowRequestRequest(creatorId = creatorId), | ||||
|         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"?> | ||||
| <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" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|     <TextView | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="50dp" | ||||
|         android:background="@color/black" | ||||
|         android:fontFamily="@font/gmarket_sans_bold" | ||||
|         android:gravity="center_vertical" | ||||
|         android:paddingHorizontal="13.3dp" | ||||
|         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_99000000">#99000000</color> | ||||
|     <color name="color_4c9970ff">#4C9970FF</color> | ||||
|     <color name="color_4dd8d8d8">#4DD8D8D8</color> | ||||
| </resources> | ||||
|   | ||||
| @@ -9,6 +9,7 @@ dependencyResolutionManagement { | ||||
|     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) | ||||
|     repositories { | ||||
|         google() | ||||
|         jcenter() | ||||
|         mavenCentral() | ||||
|         maven { url 'https://jitpack.io' } | ||||
|     } | ||||
|   | ||||