메시지 페이지 추가

This commit is contained in:
klaus 2023-08-02 14:57:16 +09:00
parent 14b652d38e
commit 3ef78b64ad
57 changed files with 3401 additions and 14 deletions

View File

@ -139,4 +139,7 @@ dependencies {
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
}

View File

@ -45,6 +45,9 @@
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

@ -17,10 +17,14 @@ object Constants {
const val EXTRA_TERMS = "extra_terms"
const val EXTRA_ROOM_ID = "extra_room_id"
const val EXTRA_USER_ID = "extra_user_id"
const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_PREV_LIVE_ROOM = "extra_prev_live_room"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response"

View File

@ -22,6 +22,13 @@ import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
import kr.co.vividnext.sodalive.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
import kr.co.vividnext.sodalive.message.SelectMessageRecipientViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageWriteViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageWriteViewModel
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.AuthApi
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
@ -94,6 +101,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) }
single { ApiBuilder().build(get(), MessageApi::class.java) }
}
private val viewModelModule = module {
@ -116,6 +124,11 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
viewModel { UserFollowerListViewModel(get(), get()) }
viewModel { TextMessageViewModel(get()) }
viewModel { TextMessageWriteViewModel(get()) }
viewModel { VoiceMessageViewModel(get()) }
viewModel { VoiceMessageWriteViewModel(get()) }
viewModel { SelectMessageRecipientViewModel(get(), get()) }
}
private val repositoryModule = module {
@ -129,6 +142,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
factory { LiveTagRepository(get()) }
factory { ReportRepository(get()) }
factory { ExplorerRepository(get()) }
factory { MessageRepository(get()) }
}
private val moduleList = listOf(

View File

@ -21,7 +21,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentExplorerBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.message.MessageSelectRecipientAdapter
import kr.co.vividnext.sodalive.message.SelectMessageRecipientAdapter
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
@ -36,7 +36,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>(
private lateinit var imm: InputMethodManager
private val handler = Handler(Looper.getMainLooper())
private lateinit var searchChannelAdapter: MessageSelectRecipientAdapter
private lateinit var searchChannelAdapter: SelectMessageRecipientAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -106,7 +106,7 @@ class ExplorerFragment : BaseFragment<FragmentExplorerBinding>(
}
private fun setupSearchChannelView() {
searchChannelAdapter = MessageSelectRecipientAdapter {
searchChannelAdapter = SelectMessageRecipientAdapter {
hideKeyboard()
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it.id)

View File

@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.live.room.StartLiveRequest
import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
import kr.co.vividnext.sodalive.live.room.create.GetRecentRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse
@ -182,4 +183,9 @@ interface LiveApi {
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetLiveRoomDonationStatusResponse>>
@GET("/live/room/recent_visit_room/users")
fun recentVisitRoomUsers(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetRoomDetailUser>>>
}

View File

@ -208,4 +208,6 @@ class LiveRepository(
roomId: Long,
token: String
) = api.donationStatus(roomId, authHeader = token)
fun recentVisitRoomUsers(token: String) = api.recentVisitRoomUsers(authHeader = token)
}

View File

@ -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
}

View File

@ -0,0 +1,100 @@
package kr.co.vividnext.sodalive.message
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
interface MessageApi {
@POST("/message/send/text")
fun sendTextMessage(
@Body request: SendTextMessageRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/message/send/voice")
@Multipart
fun sendVoiceMessage(
@Part voiceFile: MultipartBody.Part,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/message/sent/text")
fun getSentTextMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetTextMessageResponse>>
@GET("/message/received/text")
fun getReceivedTextMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetTextMessageResponse>>
@GET("/message/keep/text")
fun getKeepTextMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetTextMessageResponse>>
@GET("/message/sent/voice")
fun getSentVoiceMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetVoiceMessageResponse>>
@GET("/message/received/voice")
fun getReceivedVoiceMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetVoiceMessageResponse>>
@GET("/message/keep/voice")
fun getKeepVoiceMessage(
@Query("timezone") timezone: String,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetVoiceMessageResponse>>
@DELETE("/message/{messageId}")
fun deleteMessage(
@Path("messageId") messageId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/message/keep/text/{id}")
fun keepTextMessage(
@Path("id") id: Long,
@Body container: String = "aos",
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/message/keep/voice/{id}")
fun keepVoiceMessage(
@Path("id") id: Long,
@Body container: String = "aos",
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}

View File

@ -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
}

View File

@ -1,7 +1,65 @@
package kr.co.vividnext.sodalive.message
import android.os.Bundle
import android.view.View
import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentMessageBinding
import kr.co.vividnext.sodalive.message.text.TextMessageFragment
import kr.co.vividnext.sodalive.message.voice.VoiceMessageFragment
class MessageFragment : BaseFragment<FragmentMessageBinding>(FragmentMessageBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
changeFragment("message")
}
private fun setupView() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("문자").setTag("message"))
tabs.addTab(tabs.newTab().setText("음성").setTag("voice"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val tag = tab.tag as String
changeFragment(tag)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}
private fun changeFragment(tag: String) {
val fragmentManager = childFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val currentFragment = fragmentManager.primaryNavigationFragment
if (currentFragment != null) {
fragmentTransaction.hide(currentFragment)
}
var fragment = fragmentManager.findFragmentByTag(tag)
if (fragment == null) {
fragment = if (tag == "message") {
TextMessageFragment()
} else {
VoiceMessageFragment()
}
fragmentTransaction.add(R.id.container, fragment, tag)
} else {
fragmentTransaction.show(fragment)
}
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
}

View File

@ -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
)
}
}

View File

@ -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()
}
}
}

View File

@ -10,9 +10,9 @@ import kr.co.vividnext.sodalive.databinding.ItemSelectRecipientBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
class MessageSelectRecipientAdapter(
class SelectMessageRecipientAdapter(
private val onClickItem: (GetRoomDetailUser) -> Unit
) : RecyclerView.Adapter<MessageSelectRecipientAdapter.ViewHolder>() {
) : RecyclerView.Adapter<SelectMessageRecipientAdapter.ViewHolder>() {
inner class ViewHolder(
private val binding: ItemSelectRecipientBinding
) : RecyclerView.ViewHolder(binding.root) {

View File

@ -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())
}
)
)
}
}
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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() {
}
}

View File

@ -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 -> {
}
}
}
}
}

View File

@ -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)
}
)
)
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}
)
)
}
}

View File

@ -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()
}
}

View File

@ -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 -> {
}
}
}
}
}

View File

@ -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
}
)
)
}
}

View File

@ -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
}
}

View File

@ -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)
}
)
)
}
}

View File

@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.user
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse
@ -81,4 +82,10 @@ interface UserApi {
request: Any,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/member/search")
fun searchUser(
@Query("nickname") nickname: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetRoomDetailUser>>>
}

View File

@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.user
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
@ -62,4 +63,11 @@ class UserRepository(private val userApi: UserApi) {
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun searchUser(
nickname: String,
token: String
): Single<ApiResponse<List<GetRoomDetailUser>>> {
return userApi.searchUser(nickname, authHeader = token)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_1b1b1b" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_1b1b1b" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="16.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_777777" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_339970ff" />
<corners android:radius="6.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_339970ff" />
</shape>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="6.7dp" />
<solid android:color="@color/color_4dd8d8d8" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="6.7dp" />
<solid android:color="@color/color_4dd8d8d8" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="6.7dp" />
<solid android:color="@color/color_9970ff" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -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>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<LinearLayout
android:id="@+id/ll_profile"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:background="@drawable/bg_round_corner_10_1b1b1b"
android:gravity="center"
android:paddingVertical="12.7dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="26.7dp"
android:layout_height="26.7dp"
android:layout_marginEnd="13.3dp"
android:contentDescription="@null"
tools:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp"
tools:text="이재형 대표님" />
</LinearLayout>
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_bbbbbb"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/ll_profile"
app:layout_constraintTop_toBottomOf="@+id/ll_profile"
tools:text="2021년 7월 14일 수요일 12:00" />
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16.7dp"
android:layout_marginBottom="30dp"
android:background="@drawable/bg_round_corner_10_222222"
android:paddingHorizontal="26.7dp"
android:paddingVertical="13.3dp"
app:layout_constraintBottom_toTopOf="@+id/ll_buttons"
app:layout_constraintEnd_toEndOf="@+id/ll_profile"
app:layout_constraintStart_toStartOf="@+id/ll_profile"
app:layout_constraintTop_toBottomOf="@+id/tv_date">
<TextView
android:id="@+id/tv_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_eeeeee"
android:textSize="15sp" />
</ScrollView>
<LinearLayout
android:id="@+id/ll_buttons"
android:layout_width="0dp"
android:layout_height="48.7dp"
android:layout_marginHorizontal="13.3dp"
android:layout_marginBottom="26.7dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/tv_reply"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="답장"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" />
<TextView
android:id="@+id/tv_keep"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="6.7dp"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="보관"
android:textColor="@color/color_9970ff"
android:textSize="14.7sp" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="삭제"
android:textColor="@color/color_9970ff"
android:textSize="14.7sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:fontFamily="@font/gmarket_sans_bold"
android:text="새로운 메시지"
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp" />
<TextView
android:id="@+id/tv_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="취소"
android:textColor="@color/color_9970ff"
android:textSize="16.7sp" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_recipient"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/toolbar">
<TextView
android:id="@+id/tv_recipient_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="받는 사람"
android:textColor="@color/color_777777"
android:textSize="16.7sp" />
<TextView
android:id="@+id/tv_recipient_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="13.3dp"
android:layout_toEndOf="@+id/tv_recipient_title"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp"
tools:ignore="RelativeOverlap"
tools:text="재민" />
<ImageView
android:id="@+id/iv_select_recipient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="13.3dp"
android:contentDescription="@null"
android:src="@drawable/btn_plus_round" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="@color/color_909090" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/rl_recipient"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_10_232323_9970ff">
<EditText
android:id="@+id/et_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="top"
android:hint="내용을 입력해 주세요"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:minHeight="239dp"
android:padding="20dp"
android:textColor="@color/color_eeeeee"
android:textColorHint="@color/color_777777"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="13.3sp"
tools:ignore="LabelFor" />
</ScrollView>
<TextView
android:id="@+id/tv_send"
android:layout_width="match_parent"
android:layout_height="48.7dp"
android:layout_alignParentBottom="true"
android:layout_marginHorizontal="13.3dp"
android:layout_marginBottom="13.3dp"
android:background="@drawable/bg_round_corner_6_7_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="메시지 보내기"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" />
</RelativeLayout>

View File

@ -1,16 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/black"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center_vertical"
android:paddingHorizontal="13.3dp"
android:text="메시지"
app:layout_constraintTop_toTopOf="parent" />
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="50dp"
app:tabIndicatorColor="@color/color_9970ff"
app:tabIndicatorFullWidth="true"
app:tabIndicatorHeight="1.3dp"
app:tabSelectedTextColor="@color/color_eeeeee"
app:tabTextAppearance="@style/tabText"
app:tabTextColor="@color/color_777777" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/color_88909090" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_notice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="20dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="※ 보관하지 않은 받은 메시지는 3일 후, 자동 삭제됩니다."
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll_filter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_notice">
<TextView
android:id="@+id/tv_receive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="받은 메시지"
android:textColor="@color/color_777777"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_sent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="보낸 메시지"
android:textColor="@color/color_777777"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="보관함"
android:textColor="@color/color_777777"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_no_items"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="13.3dp"
android:background="@drawable/bg_round_corner_4_7_2b2635"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_filter">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_no_item" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:lineSpacingExtra="4sp"
android:text="메시지가 없습니다.\n친구들과 소통해보세요!"
android:textColor="@color/color_bbbbbb"
android:textSize="10.7sp"
tools:ignore="SmallSp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_message"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="13.3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_filter" />
<ImageView
android:id="@+id/iv_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16.7dp"
android:layout_marginBottom="80dp"
android:background="@drawable/bg_round_corner_33_3_9970ff"
android:contentDescription="@null"
android:padding="13.3dp"
android:src="@drawable/ic_make_message"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_notice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="20dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="※ 보관하지 않은 받은 메시지는 3일 후, 자동 삭제됩니다."
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll_filter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_notice">
<TextView
android:id="@+id/tv_receive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="받은 메시지"
android:textColor="@color/color_777777"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_sent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6.7dp"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="보낸 메시지"
android:textColor="@color/color_777777"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_16_7_transparent_777777"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="25dp"
android:paddingVertical="10.7dp"
android:text="보관함"
android:textColor="@color/color_777777"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_no_items"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="13.3dp"
android:background="@drawable/bg_round_corner_4_7_2b2635"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_filter">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_no_item" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:lineSpacingExtra="4sp"
android:text="메시지가 없습니다.\n친구들과 소통해보세요!"
android:textColor="@color/color_bbbbbb"
android:textSize="10.7sp"
tools:ignore="SmallSp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_message"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="13.3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_filter" />
<ImageView
android:id="@+id/iv_write"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16.7dp"
android:layout_marginBottom="80dp"
android:background="@drawable/bg_round_corner_33_3_9970ff"
android:contentDescription="@null"
android:padding="13.3dp"
android:src="@drawable/ic_make_voice"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="@color/color_222222">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:paddingHorizontal="26.7dp"
android:paddingTop="26.7dp"
android:text="음성메시지"
android:textColor="@color/white"
android:textSize="18.3sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingHorizontal="26.7dp"
android:paddingTop="26.7dp"
android:src="@drawable/ic_close_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/rl_select_recipient"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:background="@drawable/bg_round_corner_6_7_339970ff"
android:orientation="horizontal"
android:padding="13.3dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_close">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="46.7dp"
android:layout_height="46.7dp"
android:contentDescription="@null"
android:src="@drawable/img_thumb_default" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="13.3dp"
android:layout_toStartOf="@+id/iv_plus"
android:layout_toEndOf="@+id/iv_profile"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="TO."
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/gmarket_sans_light"
android:text="받는 사람"
android:textColor="@color/color_bbbbbb"
android:textSize="16.7sp" />
</LinearLayout>
<ImageView
android:id="@+id/iv_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/btn_plus_round" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="81dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rl_select_recipient">
<TextView
android:id="@+id/tv_timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_light"
android:text="00:00:00"
android:textColor="@color/white"
android:textSize="33.3sp" />
<com.gauravk.audiovisualizer.visualizer.WaveVisualizer
android:id="@+id/sound_visualizer"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_marginHorizontal="13.3dp"
android:visibility="gone"
app:avColor="@color/av_deep_orange"
app:avDensity="0.8"
app:avSpeed="normal"
app:avType="fill" />
<ImageView
android:id="@+id/iv_record_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="52.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_record" />
<ImageView
android:id="@+id/iv_record_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="52.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_stop"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/rl_record_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<ImageView
android:id="@+id/iv_record_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="90dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_play" />
<ImageView
android:id="@+id/iv_record_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="90dp"
android:contentDescription="@null"
android:src="@drawable/ic_record_pause"
android:visibility="gone" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="60dp"
android:layout_toEndOf="@+id/iv_record_play"
android:fontFamily="@font/gmarket_sans_medium"
android:text="삭제"
android:textColor="@color/color_bbbbbb"
android:textSize="15.3sp" />
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_retry_or_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="26.7dp"
android:layout_marginBottom="13.3dp"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/tv_retry_record"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_10_339970ff_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="다시 녹음"
android:textColor="@color/color_9970ff"
android:textSize="18.3sp" />
<TextView
android:id="@+id/tv_send_message"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="13.3dp"
android:layout_weight="2"
android:background="@drawable/bg_round_corner_10_9970ff"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:text="메시지 보내기"
android:textColor="@color/white"
android:textSize="18.3sp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="46.7dp"
android:layout_height="46.7dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="13.3dp"
android:layout_marginEnd="35.3dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_date"
app:layout_constraintStart_toEndOf="@+id/iv_profile"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
android:maxLines="1"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
tools:text="dlksjfei" />
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_light"
android:maxLines="1"
android:textColor="@color/color_777777"
android:textSize="12sp"
tools:text="마지막 메세지 내용. 네 감사합니다. 그럼 다음에 또 연락드릴께요~!" />
</LinearLayout>
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:fontFamily="@font/gmarket_sans_light"
android:gravity="center"
android:textColor="@color/color_525252"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="8월 04일" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/black">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="46.7dp"
android:layout_height="46.7dp"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_nickname"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:layout_marginEnd="35.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_eeeeee"
android:textSize="13.3sp"
app:layout_constraintBottom_toBottomOf="@+id/iv_profile"
app:layout_constraintEnd_toStartOf="@+id/tv_date"
app:layout_constraintStart_toEndOf="@+id/iv_profile"
app:layout_constraintTop_toTopOf="@+id/iv_profile"
tools:text="dlksjfei" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:fontFamily="@font/gmarket_sans_light"
android:gravity="center"
android:textColor="@color/color_525252"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/iv_profile"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_profile"
tools:text="8월 04일" />
<LinearLayout
android:id="@+id/ll_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/bg_round_corner_6_7_339970ff"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="20dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_profile">
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progressDrawable="@drawable/voice_message_player_seekbar"
android:thumb="@null" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6.7dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="00:00"
android:textColor="@color/color_bbbbbb"
android:textSize="10.7sp"
tools:ignore="SmallSp" />
<TextView
android:id="@+id/tv_total_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="00:00"
android:textColor="@color/color_bbbbbb"
android:textSize="10.7sp"
tools:ignore="SmallSp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24.3dp">
<ImageView
android:id="@+id/iv_keep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="13.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_save"
android:visibility="gone" />
<ImageView
android:id="@+id/iv_play_or_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:contentDescription="@null"
android:src="@drawable/btn_bar_play" />
<ImageView
android:id="@+id/iv_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="13.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_delete"
android:visibility="gone" />
<ImageView
android:id="@+id/iv_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="13.3dp"
android:contentDescription="@null"
android:src="@drawable/ic_mic_paint"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -68,4 +68,5 @@
<color name="color_ffb600">#FFB600</color>
<color name="color_99000000">#99000000</color>
<color name="color_4c9970ff">#4C9970FF</color>
<color name="color_4dd8d8d8">#4DD8D8D8</color>
</resources>

View File

@ -9,6 +9,7 @@ dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
jcenter()
mavenCentral()
maven { url 'https://jitpack.io' }
}