diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f4cf935a..368e154c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,9 @@
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:targetApi="31">
+
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerActivity.kt
new file mode 100644
index 00000000..4822f210
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerActivity.kt
@@ -0,0 +1,99 @@
+package kr.co.vividnext.sodalive.v2.main.chat.recipient
+
+import androidx.activity.viewModels
+import androidx.core.view.isVisible
+import androidx.core.widget.doAfterTextChanged
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.ActivityDmRecipientPickerBinding
+import kr.co.vividnext.sodalive.explorer.ExplorerRepository
+import kr.co.vividnext.sodalive.settings.notification.MemberRole
+import kr.co.vividnext.sodalive.user.UserRepository
+import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
+import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
+import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
+import org.koin.android.ext.android.inject
+
+class DmRecipientPickerActivity : BaseActivity(
+ ActivityDmRecipientPickerBinding::inflate
+) {
+ private val explorerRepository: ExplorerRepository by inject()
+ private val userRepository: UserRepository by inject()
+ private val dmRepository: DmChatRepository by inject()
+ private val viewModel: DmRecipientPickerViewModel by viewModels {
+ dmRecipientPickerFactory(explorerRepository, userRepository)
+ }
+ private val adapter = DmRecipientAdapter(::confirmRecipient)
+ private val loadingDialog by lazy { LoadingDialog(this, layoutInflater) }
+ private var isOpeningRoom = false
+
+ override fun setupView() {
+ if (SharedPreferenceManager.role != MemberRole.CREATOR.name) {
+ finish()
+ return
+ }
+ binding.btnBack.setOnClickListener { finish() }
+ binding.rvRecipients.layoutManager = LinearLayoutManager(this)
+ binding.rvRecipients.adapter = adapter
+ binding.etSearchNickname.doAfterTextChanged {
+ viewModel.onSearchQueryChanged(it?.toString().orEmpty())
+ }
+ viewModel.recipientStateLiveData.observe(this) { state ->
+ adapter.submitItems(state.recipients)
+ binding.tvFollowers.isVisible = state.mode == DmRecipientPickerMode.FOLLOWERS
+ binding.tvSearchResultCount.isVisible = state.mode == DmRecipientPickerMode.SEARCH
+ binding.tvSearchResultCount.text = getString(
+ R.string.dm_recipient_search_result_count,
+ state.searchResultCount
+ )
+ binding.tvSearchEmpty.isVisible = state.isSearchEmpty
+ binding.rvRecipients.isVisible = !state.isSearchEmpty
+ if (state.isError) showToast(getString(R.string.common_error_request))
+ }
+ viewModel.enter()
+ }
+
+ private fun confirmRecipient(recipient: DmRecipientUiModel) {
+ if (isOpeningRoom) return
+ V2ModalDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ title = getString(R.string.dm_recipient_confirm_title),
+ desc = getString(R.string.dm_recipient_confirm_message, recipient.nickname),
+ cancelButtonTitle = getString(R.string.cancel),
+ confirmButtonTitle = getString(R.string.dm_recipient_send),
+ confirmButtonClick = { openRoom(recipient.id) }
+ ).show(screenWidth)
+ }
+
+ private fun openRoom(recipientId: Long) {
+ if (isOpeningRoom) return
+ isOpeningRoom = true
+ loadingDialog.show(screenWidth)
+ compositeDisposable.add(
+ dmRepository.createOrGetRoomByRecipientId(SharedPreferenceManager.token, recipientId)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .doFinally {
+ isOpeningRoom = false
+ loadingDialog.dismiss()
+ }
+ .subscribe({ response ->
+ val data = response.data
+ if (response.success && data != null) {
+ startActivity(DmChatRoomActivity.newIntentByRoomId(this, data.roomId))
+ finish()
+ } else {
+ showToast(getString(R.string.common_error_request))
+ }
+ }, {
+ showToast(getString(R.string.common_error_request))
+ })
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerFactory.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerFactory.kt
new file mode 100644
index 00000000..3d7b2492
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerFactory.kt
@@ -0,0 +1,40 @@
+package kr.co.vividnext.sodalive.v2.main.chat.recipient
+
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.explorer.ExplorerRepository
+import kr.co.vividnext.sodalive.user.UserRepository
+
+internal fun dmRecipientPickerFactory(
+ explorerRepository: ExplorerRepository,
+ userRepository: UserRepository
+) = viewModelFactory {
+ initializer {
+ DmRecipientPickerViewModel(
+ loadFollowers = {
+ explorerRepository.getFollowerList(
+ userId = SharedPreferenceManager.userId,
+ page = 1,
+ size = 20,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ ).map { response ->
+ val data = response.data
+ check(response.success && data != null)
+ data.items.map { DmRecipientUiModel(it.userId, it.nickname, it.profileImage) }
+ }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ },
+ searchUsers = { nickname ->
+ userRepository.searchUser(nickname, "Bearer ${SharedPreferenceManager.token}")
+ .map { response ->
+ val data = response.data
+ check(response.success && data != null)
+ data.map { DmRecipientUiModel(it.id, it.nickname, it.profileImageUrl) }
+ }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+ },
+ debounceScheduler = AndroidSchedulers.mainThread()
+ )
+ }
+}
diff --git a/app/src/main/res/layout/activity_dm_recipient_picker.xml b/app/src/main/res/layout/activity_dm_recipient_picker.xml
new file mode 100644
index 00000000..943b66ae
--- /dev/null
+++ b/app/src/main/res/layout/activity_dm_recipient_picker.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+