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