From 127e5fc1470c48d8513aef109722e20bbc48067f Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 14 Sep 2026 22:05:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(dm):=20=EC=88=98=EC=8B=A0=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 3 + .../recipient/DmRecipientPickerActivity.kt | 99 +++++++++++++++++ .../recipient/DmRecipientPickerFactory.kt | 40 +++++++ .../layout/activity_dm_recipient_picker.xml | 105 ++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerActivity.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/recipient/DmRecipientPickerFactory.kt create mode 100644 app/src/main/res/layout/activity_dm_recipient_picker.xml 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +