diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c2f6af4..9e573ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,6 +41,10 @@ + + + + >> + + @GET("/explorer/profile/{id}") + fun getCreatorProfile( + @Path("id") id: Long, + @Query("timezone") timezone: String, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/explorer/profile/cheers") + fun writeCheers( + @Body request: PostWriteCheersRequest, + @Header("Authorization") authHeader: String + ): Single> + + @PUT("/explorer/profile/cheers") + fun modifyCheers( + @Body request: PutModifyCheersRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/explorer/profile/notice") + fun writeCreatorNotice( + @Body request: PostCreatorNoticeRequest, + @Header("Authorization") authHeader: String + ): Single> + + @GET("/explorer/profile/{id}/follower-list") + fun getFollowerList( + @Path("id") userId: Long, + @Query("page") page: Int, + @Query("size") size: Int, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt index 20e3984..c461412 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt @@ -1,5 +1,10 @@ package kr.co.vividnext.sodalive.explorer +import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest +import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest +import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest +import java.util.TimeZone + class ExplorerRepository( private val api: ExplorerApi ) { @@ -9,4 +14,53 @@ class ExplorerRepository( channel = channel, authHeader = token ) + + fun getCreatorProfile(id: Long, token: String) = api.getCreatorProfile( + id = id, + timezone = TimeZone.getDefault().id, + authHeader = token + ) + + fun writeCheers( + parentCheersId: Long?, + creatorId: Long, + content: String, + token: String + ) = api.writeCheers( + request = PostWriteCheersRequest( + parentId = parentCheersId, + creatorId = creatorId, + content = content + ), + authHeader = token + ) + + fun modifyCheers( + cheersId: Long, + content: String, + token: String + ) = api.modifyCheers( + request = PutModifyCheersRequest( + cheersId = cheersId, + content = content + ), + authHeader = token + ) + + fun writeCreatorNotice(notice: String, token: String) = api.writeCreatorNotice( + request = PostCreatorNoticeRequest(notice), + authHeader = token + ) + + fun getFollowerList( + userId: Long, + page: Int, + size: Int, + token: String + ) = api.getFollowerList( + userId = userId, + page = page - 1, + size = size, + authHeader = token + ) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorNoticeWriteActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorNoticeWriteActivity.kt new file mode 100644 index 0000000..8436ef8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/CreatorNoticeWriteActivity.kt @@ -0,0 +1,73 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import android.content.Intent +import android.widget.Toast +import com.orhanobut.logger.Logger +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.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.ActivityCreatorNoticeWriteBinding +import kr.co.vividnext.sodalive.explorer.ExplorerRepository +import org.koin.android.ext.android.inject + +class CreatorNoticeWriteActivity : BaseActivity( + ActivityCreatorNoticeWriteBinding::inflate +) { + + private val repository: ExplorerRepository by inject() + + private lateinit var loadingDialog: LoadingDialog + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "공지사항 쓰기" + binding.toolbar.tvBack.setOnClickListener { finish() } + + val notice = intent.getStringExtra("notice") + binding.etContent.setText(notice) + + binding.tvSave.setOnClickListener { + loadingDialog.show(screenWidth) + + val writtenNotice = binding.etContent.text.toString() + compositeDisposable.add( + repository.writeCreatorNotice( + notice = writtenNotice, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + loadingDialog.dismiss() + + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + Toast.makeText( + applicationContext, + message, + Toast.LENGTH_LONG + ).show() + + if (it.success) { + val dataIntent = Intent() + dataIntent.putExtra("notice", writtenNotice) + setResult(RESULT_OK, dataIntent) + finish() + } + }, + { + loadingDialog.dismiss() + it.message?.let { message -> Logger.e(message) } + Toast.makeText( + applicationContext, + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.", + Toast.LENGTH_LONG + ).show() + } + ) + ) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCheersResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCheersResponse.kt new file mode 100644 index 0000000..02d4b34 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCheersResponse.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import com.google.gson.annotations.SerializedName + +data class GetCheersResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("cheers") val cheers: List +) + +data class GetCheersResponseItem( + @SerializedName("cheersId") val cheersId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("content") val content: String, + @SerializedName("date") val date: String, + @SerializedName("replyList") val replyList: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt new file mode 100644 index 0000000..613ef9e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/GetCreatorProfileResponse.kt @@ -0,0 +1,95 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import com.google.gson.annotations.SerializedName + +data class GetCreatorProfileResponse( + @SerializedName("creator") + val creator: CreatorResponse, + @SerializedName("userDonationRanking") + val userDonationRanking: List, + @SerializedName("similarCreatorList") + val similarCreatorList: List, + @SerializedName("liveRoomList") + val liveRoomList: List, + @SerializedName("audioContentList") + val audioContentList: List, + @SerializedName("notice") + val notice: String, + @SerializedName("cheers") + val cheers: GetCheersResponse, + @SerializedName("activitySummary") + val activitySummary: GetCreatorActivitySummary, + @SerializedName("isBlock") + val isBlock: Boolean +) + +data class CreatorResponse( + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("tags") val tags: List, + @SerializedName("introduce") val introduce: String = "", + @SerializedName("instagramUrl") val instagramUrl: String? = null, + @SerializedName("youtubeUrl") val youtubeUrl: String? = null, + @SerializedName("websiteUrl") val websiteUrl: String? = null, + @SerializedName("blogUrl") val blogUrl: String? = null, + @SerializedName("isAvailableChat") val isAvailableChat: Boolean = true, + @SerializedName("isNotification") val isNotification: Boolean, + @SerializedName("notificationRecipientCount") val notificationRecipientCount: Int +) + +data class UserDonationRankingResponse( + @SerializedName("userId") val userId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImage") val profileImage: String, + @SerializedName("donationCoin") val donationCoin: Int +) + +data class SimilarCreatorResponse( + @SerializedName("userId") val userId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImage") val profileImage: String, + @SerializedName("tags") val tags: List +) + +data class LiveRoomResponse( + @SerializedName("roomId") val roomId: Long, + @SerializedName("title") val title: String, + @SerializedName("content") val content: String, + @SerializedName("isPaid") val isPaid: Boolean, + @SerializedName("beginDateTime") val beginDateTime: String, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("isAdult") val isAdult: Boolean, + @SerializedName("price") val price: Int, + @SerializedName("channelName") val channelName: String?, + @SerializedName("managerNickname") val managerNickname: String, + @SerializedName("isReservation") val isReservation: Boolean, + @SerializedName("isActive") val isActive: Boolean, + @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean +) + +data class GetAudioContentListResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List +) + +data class GetAudioContentListItem( + @SerializedName("contentId") val contentId: Long, + @SerializedName("coverImageUrl") val coverImageUrl: String, + @SerializedName("title") val title: String, + @SerializedName("price") val price: Int, + @SerializedName("themeStr") val themeStr: String, + @SerializedName("duration") val duration: String?, + @SerializedName("likeCount") val likeCount: Int, + @SerializedName("commentCount") val commentCount: Int, + @SerializedName("isAdult") val isAdult: Boolean +) + +data class GetCreatorActivitySummary( + @SerializedName("liveCount") val liveCount: Int, + @SerializedName("liveTime") val liveTime: Int, + @SerializedName("liveContributorCount") val liveContributorCount: Int, + @SerializedName("contentCount") val contentCount: Int +) + + diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt new file mode 100644 index 0000000..b9a42ee --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import com.google.gson.annotations.SerializedName + +data class PostCreatorNoticeRequest( + @SerializedName("notice") + val notice: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt index 6719175..aeb8fca 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt @@ -1,11 +1,773 @@ package kr.co.vividnext.sodalive.explorer.profile +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.app.Service +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.webkit.URLUtil +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R 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.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding +import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter +import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter +import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity +import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity +import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.live.LiveViewModel +import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity +import kr.co.vividnext.sodalive.live.room.LiveRoomActivity +import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog +import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog +import kr.co.vividnext.sodalive.report.CheersReportDialog +import kr.co.vividnext.sodalive.report.ProfileReportDialog +import kr.co.vividnext.sodalive.report.ReportType +import kr.co.vividnext.sodalive.report.UserReportDialog +import org.koin.android.ext.android.inject -class UserProfileActivity: BaseActivity( +class UserProfileActivity : BaseActivity( ActivityUserProfileBinding::inflate ) { + + private val viewModel: UserProfileViewModel by inject() + private val liveViewModel: LiveViewModel by inject() + + private lateinit var imm: InputMethodManager + private lateinit var loadingDialog: LoadingDialog + private lateinit var liveAdapter: UserProfileLiveAdapter + private lateinit var donationAdapter: UserProfileDonationAdapter + private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter + private lateinit var cheersAdapter: UserProfileCheersAdapter + + private lateinit var noticeWriteLauncher: ActivityResultLauncher + + private val handler = Handler(Looper.getMainLooper()) + private var userId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0) + super.onCreate(savedInstanceState) + + imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager + noticeWriteLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + val writtenNotice = it.data?.getStringExtra("notice") + binding.tvNotice.text = writtenNotice?.ifBlank { + "공지사항이 없습니다." + } + } + } + + if (userId <= 0) { + Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show() + finish() + } + bindData() + } + + override fun onResume() { + super.onResume() + viewModel.getCreatorProfile(userId) { finish() } + } + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.tvBack.text = "채널" + binding.tvBack.setOnClickListener { finish() } + binding.ivMenu.setOnClickListener { + showOptionMenu( + this, + binding.ivMenu, + ) + } + binding.layoutUserProfile.ivShare.setOnClickListener { + viewModel.shareChannel(userId = userId) { + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, it) + + val shareIntent = Intent.createChooser(intent, "채널 공유") + startActivity(shareIntent) + } + } + + setupLiveView() + setupDonationView() + setupSimilarCreatorView() + setupFanTalkView() + } + + private fun hideKeyboard(onAfterExecute: () -> Unit) { + handler.postDelayed({ + imm.hideSoftInputFromWindow( + window.decorView.applicationWindowToken, + InputMethodManager.HIDE_NOT_ALWAYS + ) + onAfterExecute() + }, 100) + } + + private fun showOptionMenu(context: Context, v: View) { + val popup = PopupMenu(context, v) + val inflater = popup.menuInflater + + if (viewModel.creatorProfileLiveData.value!!.isBlock) { + inflater.inflate(R.menu.user_profile_option_menu_2, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_user_block -> { + viewModel.userUnBlock(userId) + } + + R.id.menu_user_report -> { + showUserReportDialog() + } + + R.id.menu_profile_report -> { + showProfileReportDialog() + } + } + + true + } + } else { + inflater.inflate(R.menu.user_profile_option_menu, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_user_block -> { + showUserBlockDialog() + } + + R.id.menu_user_report -> { + showUserReportDialog() + } + + R.id.menu_profile_report -> { + showProfileReportDialog() + } + } + + true + } + } + + popup.show() + } + + private fun showUserBlockDialog() { + val dialog = AlertDialog.Builder(this) + dialog.setTitle("사용자 차단") + dialog.setMessage( + "${binding.layoutUserProfile.tvNickname.text}님을 차단하시겠습니까?\n\n" + + "사용자를 차단하면 사용자는 아래 기능이 제한됩니다.\n" + + "- 내가 개설한 라이브 입장 불가\n" + + "- 나에게 메시지 보내기 불가\n" + + "- 내 채널의 팬Talk 작성불가" + ) + dialog.setPositiveButton("차단") { _, _ -> + viewModel.userBlock(userId) + } + dialog.setNegativeButton("취소") { _, _ -> } + dialog.show() + } + + private fun showUserReportDialog() { + val dialog = UserReportDialog(this, layoutInflater) { + viewModel.report( + type = ReportType.USER, + userId = userId, + reason = it + ) + } + + dialog.show(screenWidth) + } + + private fun showProfileReportDialog() { + val dialog = ProfileReportDialog(this, layoutInflater) { + viewModel.report( + type = ReportType.PROFILE, + userId = userId + ) + } + + dialog.show(screenWidth) + } + + private fun setupLiveView() { + val recyclerView = binding.layoutUserProfileLive.rvLive + liveAdapter = UserProfileLiveAdapter( + onClickParticipant = { enterLiveRoom(roomId = it.roomId) }, + onClickReservation = { reservationRoom(roomId = it.roomId) } + ) + + 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) + + when (parent.getChildAdapterPosition(view)) { + liveAdapter.itemCount - 1 -> { + outRect.bottom = 0 + } + + else -> { + outRect.bottom = 13.3f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = liveAdapter + } + + private fun setupDonationView() { + binding.layoutUserProfileDonation.tvAll.setOnClickListener { + val intent = Intent(applicationContext, UserProfileDonationAllViewActivity::class.java) + intent.putExtra(Constants.EXTRA_USER_ID, userId) + startActivity(intent) + } + + val recyclerView = binding.layoutUserProfileDonation.rvDonation + donationAdapter = UserProfileDonationAdapter() + + recyclerView.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.HORIZONTAL, + false + ) + + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + donationAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = donationAdapter + } + + private fun setupSimilarCreatorView() { + val recyclerView = binding.layoutUserProfileSimilarCreator.rvSimilarCreator + similarCreatorAdapter = UserProfileSimilarCreatorAdapter { + val intent = Intent(applicationContext, UserProfileActivity::class.java) + intent.putExtra(Constants.EXTRA_USER_ID, it.userId) + startActivity(intent) + } + + 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) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 0 + outRect.bottom = 10f.dpToPx().toInt() + } + + similarCreatorAdapter.itemCount - 1 -> { + outRect.top = 10f.dpToPx().toInt() + outRect.bottom = 0 + } + + else -> { + outRect.top = 10f.dpToPx().toInt() + outRect.bottom = 10f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = similarCreatorAdapter + } + + private fun setupFanTalkView() { + binding.layoutUserProfileFanTalk.tvAll.setOnClickListener { + val intent = Intent( + applicationContext, + UserProfileFantalkAllViewActivity::class.java + ) + intent.putExtra(Constants.EXTRA_USER_ID, userId) + startActivity(intent) + } + + setupCheersView() + } + + private fun setupCheersView() { + binding.layoutUserProfileFanTalk.ivSend.setOnClickListener { + hideKeyboard { + viewModel.writeCheers( + creatorId = userId, + cheersContent = binding.layoutUserProfileFanTalk.etCheer.text.toString() + ) + } + } + + val rvCheers = binding.layoutUserProfileFanTalk.rvCheers + cheersAdapter = UserProfileCheersAdapter( + userId = userId, + enterReply = { cheersId, content -> + hideKeyboard { + viewModel.writeCheers( + parentCheersId = cheersId, + creatorId = userId, + cheersContent = content + ) + } + }, + modifyReply = { cheersId, content -> + hideKeyboard { + viewModel.modifyCheers( + cheersId = cheersId, + creatorId = userId, + cheersContent = content + ) + } + }, + onClickReport = { showCheersReportPopup(it) } + ) + + rvCheers.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + rvCheers.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.bottom = 0 + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.top = 0 + } + + cheersAdapter.itemCount - 1 -> { + outRect.top = 10.dpToPx().toInt() + outRect.bottom = 10.dpToPx().toInt() + } + + else -> { + outRect.top = 10.dpToPx().toInt() + } + } + } + }) + + rvCheers.adapter = cheersAdapter + } + + private fun showCheersReportPopup(cheersId: Long) { + val dialog = CheersReportDialog(this, layoutInflater) { + if (it.isBlank()) { + Toast.makeText( + applicationContext, + "신고 이유를 선택해 주세요.", + Toast.LENGTH_LONG + ).show() + } else { + viewModel.cheersReport(cheersId, reason = it) + } + } + + dialog.show(screenWidth) + } + + private fun bindData() { + liveViewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + liveViewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + 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() + } + } + + viewModel.creatorProfileLiveData.observe(this) { + setCheers(it.cheers) + setCreatorProfile(it.creator) + setCreatorNotice(it.notice, it.creator.creatorId) + setLiveRoomList(it.liveRoomList) + setSimilarCreatorList(it.similarCreatorList) + setUserDonationRanking(it.userDonationRanking) + setActivitySummary(it.activitySummary) + } + + viewModel.isExpandNotice.observe(this) { + if (it) { + binding.tvNotice.maxLines = Int.MAX_VALUE + } else { + binding.tvNotice.maxLines = 1 + } + } + } + + private fun setActivitySummary(activitySummary: GetCreatorActivitySummary) { + binding.tvLiveCount.text = activitySummary.liveCount.moneyFormat() + binding.tvLiveContributorCount.text = activitySummary.liveContributorCount.moneyFormat() + binding.tvLiveTime.text = activitySummary.liveTime.moneyFormat() + binding.tvContentCount.text = activitySummary.contentCount.moneyFormat() + } + + @SuppressLint("NotifyDataSetChanged") + private fun setCheers(cheers: GetCheersResponse) { + binding.layoutUserProfileFanTalk.etCheer.setText("") + cheersAdapter.items.clear() + binding.layoutUserProfileFanTalk.tvCheersCount.text = cheers.totalCount.toString() + cheersAdapter.items.addAll(cheers.cheers) + cheersAdapter.notifyDataSetChanged() + + if (cheersAdapter.itemCount <= 0) { + binding.layoutUserProfileFanTalk.rvCheers.visibility = View.GONE + binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.VISIBLE + } else { + binding.layoutUserProfileFanTalk.rvCheers.visibility = View.VISIBLE + binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.GONE + } + } + + @SuppressLint("SetTextI18n") + private fun setCreatorProfile(creator: CreatorResponse) { + val layoutUserProfile = binding.layoutUserProfile + + if (creator.creatorId == SharedPreferenceManager.userId) { + layoutUserProfile.tvFollowerList.visibility = View.VISIBLE + layoutUserProfile.llNotification.visibility = View.GONE + + layoutUserProfile.tvFollowerList.setOnClickListener { + val intent = Intent(applicationContext, UserFollowerListActivity::class.java) + intent.putExtra(Constants.EXTRA_USER_ID, creator.creatorId) + startActivity(intent) + } + } else { + layoutUserProfile.llNotification.visibility = View.VISIBLE + layoutUserProfile.tvFollowerList.visibility = View.GONE + } + + layoutUserProfile.ivProfile.load(creator.profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.tvBack.text = "${creator.nickname}님의 채널" + layoutUserProfile.tvNickname.text = creator.nickname + layoutUserProfile.tvTags.text = creator.tags.joinToString(" ") { "#$it" } + + if (creator.websiteUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.websiteUrl)) { + layoutUserProfile.ivWebsite.visibility = View.GONE + } else { + layoutUserProfile.ivWebsite.visibility = View.VISIBLE + layoutUserProfile.ivWebsite.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.websiteUrl))) + } + } + + if (creator.blogUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.blogUrl)) { + layoutUserProfile.ivBlog.visibility = View.GONE + } else { + layoutUserProfile.ivBlog.visibility = View.VISIBLE + layoutUserProfile.ivBlog.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.blogUrl))) + } + } + + if (creator.instagramUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.instagramUrl)) { + layoutUserProfile.ivInstagram.visibility = View.GONE + } else { + layoutUserProfile.ivInstagram.visibility = View.VISIBLE + layoutUserProfile.ivInstagram.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.instagramUrl))) + } + } + + if (creator.youtubeUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.youtubeUrl)) { + layoutUserProfile.ivYoutube.visibility = View.GONE + } else { + layoutUserProfile.ivYoutube.visibility = View.VISIBLE + layoutUserProfile.ivYoutube.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.youtubeUrl))) + } + } + + if (creator.isNotification) { + layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification_selected) + layoutUserProfile.ivNotification.setOnClickListener { + viewModel.unFollow(creator.creatorId) + } + } else { + layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification) + layoutUserProfile.ivNotification.setOnClickListener { + viewModel.follow(creator.creatorId) + } + } + + layoutUserProfile + .tvNotificationCount + .text = "팔로워 ${creator.notificationRecipientCount.moneyFormat()}명" + + val introduce = creator.introduce.ifBlank { + "채널 소개내용이 없습니다." + } + + binding.layoutUserProfileIntroduce.tvIntroduce.text = introduce + } + + private fun setCreatorNotice(notice: String, creatorId: Long) { + binding.tvNotice.text = notice.ifBlank { + "공지사항이 없습니다." + } + + binding.rlNotice.setOnClickListener { + if (creatorId == SharedPreferenceManager.userId) { + val intent = Intent(applicationContext, CreatorNoticeWriteActivity::class.java) + intent.putExtra("notice", notice) + noticeWriteLauncher.launch(intent) + } else { + viewModel.toggleExpandNotice() + } + } + + binding.ivWrite.visibility = if (creatorId == SharedPreferenceManager.userId) { + View.VISIBLE + } else { + View.GONE + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun setLiveRoomList(liveRoomList: List) { + if (liveRoomList.isEmpty()) { + binding.layoutUserProfileLive.root.visibility = View.GONE + } else { + binding.layoutUserProfileLive.root.visibility = View.VISIBLE + liveAdapter.items.clear() + liveAdapter.items.addAll(liveRoomList) + liveAdapter.notifyDataSetChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun setSimilarCreatorList(similarCreatorList: List) { + if (similarCreatorList.isEmpty()) { + binding.llUserProfileSimilarCreator.visibility = View.GONE + } else { + binding.llUserProfileSimilarCreator.visibility = View.VISIBLE + similarCreatorAdapter.items.clear() + similarCreatorAdapter.items.addAll(similarCreatorList) + similarCreatorAdapter.notifyDataSetChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun setUserDonationRanking(userDonationRanking: List) { + if (userDonationRanking.isEmpty()) { + binding.llUserProfileDonation.visibility = View.GONE + } else { + binding.llUserProfileDonation.visibility = View.VISIBLE + donationAdapter.items.clear() + donationAdapter.items.addAll(userDonationRanking) + donationAdapter.notifyDataSetChanged() + } + } + + private fun reservationRoom(roomId: Long) { + liveViewModel.getRoomDetail(roomId) { + if (it.manager.id == SharedPreferenceManager.userId) { + showToast("내가 만든 라이브는 예약할 수 없습니다.") + } else { + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = this, + layoutInflater = layoutInflater, + can = if (it.isPaid) 0 else it.price, + confirmButtonClick = { password -> + handler.postDelayed({ + processLiveReservation(roomId, password) + }, 300) + } + ).show(screenWidth) + } else { + if (it.price == 0 || it.isPaid) { + processLiveReservation(roomId) + } else { + LivePaymentDialog( + activity = this, + layoutInflater = layoutInflater, + title = "${it.price.moneyFormat()}캔으로 예약", + desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.", + confirmButtonTitle = "예약하기", + confirmButtonClick = { processLiveReservation(roomId) }, + cancelButtonTitle = "취소", + cancelButtonClick = {} + ).show(screenWidth) + } + } + } + } + } + + private fun processLiveReservation(roomId: Long, password: String? = null) { + liveViewModel.reservationRoom(roomId, password) { + val intent = Intent( + applicationContext, + LiveReservationCompleteActivity::class.java + ) + intent.putExtra(Constants.EXTRA_LIVE_RESERVATION_RESPONSE, it) + startActivity(intent) + } + } + + private fun enterLiveRoom(roomId: Long) { + val onEnterRoomSuccess = { + runOnUiThread { + val intent = Intent(applicationContext, LiveRoomActivity::class.java) + intent.putExtra(Constants.EXTRA_ROOM_ID, roomId) + startActivity(intent) + } + } + + liveViewModel.getRoomDetail(roomId) { + if (it.channelName != null) { + if (it.manager.id == SharedPreferenceManager.userId) { + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + } else if (it.price == 0 || it.isPaid) { + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = this, + layoutInflater = layoutInflater, + can = 0, + confirmButtonClick = { password -> + liveViewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidth) + } else { + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + } + } else { + if (it.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = this, + layoutInflater = layoutInflater, + can = it.price, + confirmButtonClick = { password -> + liveViewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidth) + } else { + LivePaymentDialog( + activity = this, + layoutInflater = layoutInflater, + title = "${it.price.moneyFormat()}캔으로 입장", + desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.", + confirmButtonTitle = "결제 후 입장", + confirmButtonClick = { + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + }, + cancelButtonTitle = "취소", + cancelButtonClick = {} + ).show(screenWidth) + } + } + } + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileLiveAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileLiveAdapter.kt new file mode 100644 index 0000000..186a45a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileLiveAdapter.kt @@ -0,0 +1,139 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemUserProfileLiveBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class UserProfileLiveAdapter( + private val onClickParticipant: (LiveRoomResponse) -> Unit, + private val onClickReservation: (LiveRoomResponse) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val context: Context, + private val binding: ItemUserProfileLiveBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") + fun bind(item: LiveRoomResponse) { + binding.ivCover.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(4.7f.dpToPx())) + } + + binding.iv19.visibility = if (item.isAdult) { + View.VISIBLE + } else { + View.GONE + } + + binding.tvDate.text = item.beginDateTime + binding.tvNickname.text = item.managerNickname + binding.tvTitle.text = item.title + + if (item.isActive && !item.channelName.isNullOrBlank()) { + binding.bgCover.visibility = View.GONE + binding.tvStatus.text = "LIVE" + binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_ff5c49)) + binding.tvStatus.setBackgroundResource( + R.drawable.bg_round_corner_3_3_transparent_ff5c49 + ) + + binding.tvParticipate.text = if (!item.isPaid && item.price > 0) { + "${item.price}캔으로 지금 참여하기" + } else { + "지금 참여하기" + } + + binding.tvParticipate.setOnClickListener { onClickParticipant(item) } + + binding.tvParticipate.setTextColor( + ContextCompat.getColor( + context, + R.color.white + ) + ) + binding.tvParticipate.setBackgroundResource( + R.drawable.bg_round_corner_5_3_dd4500 + ) + } else if (item.isActive && item.channelName.isNullOrBlank()) { + binding.bgCover.visibility = View.GONE + binding.tvStatus.text = "예정" + binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_fdca2f)) + binding.tvStatus.setBackgroundResource( + R.drawable.bg_round_corner_3_3_transparent_fdca2f + ) + + if (item.isReservation) { + binding.tvParticipate.text = "예약완료" + binding.tvParticipate.setTextColor( + ContextCompat.getColor(context, R.color.color_777777) + ) + binding.tvParticipate.setBackgroundResource( + R.drawable.bg_round_corner_5_3_525252 + ) + } else { + binding.tvParticipate.text = if (item.price > 0) { + "${item.price}캔으로 예약하기" + } else { + "예약하기" + } + + binding.tvParticipate.setOnClickListener { onClickReservation(item) } + + binding.tvParticipate.setTextColor( + ContextCompat.getColor( + context, + R.color.black + ) + ) + binding.tvParticipate.setBackgroundResource( + R.drawable.bg_round_corner_5_3_fdca2f + ) + } + } else { + binding.bgCover.visibility = View.VISIBLE + binding.tvStatus.text = "종료" + binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_777777)) + binding.tvStatus.setBackgroundResource( + R.drawable.bg_round_corner_3_3_transparent_777777 + ) + + binding.tvParticipate.text = "다시듣기를 지원하지 않습니다" + + binding.tvParticipate.setTextColor( + ContextCompat.getColor(context, R.color.color_777777) + ) + binding.tvParticipate.setBackgroundResource( + R.drawable.bg_round_corner_5_3_525252 + ) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + parent.context, + ItemUserProfileLiveBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileSimilarCreatorAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileSimilarCreatorAdapter.kt new file mode 100644 index 0000000..e4b98c5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileSimilarCreatorAdapter.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemUserProfileSimilarCreatorBinding + +class UserProfileSimilarCreatorAdapter( + private val onClickItem: (SimilarCreatorResponse) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemUserProfileSimilarCreatorBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: SimilarCreatorResponse) { + binding.ivProfile.load(item.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + binding.tvNickname.text = item.nickname + binding.tvTags.text = item.tags.joinToString(" ") { "#$it" } + binding.root.setOnClickListener { onClickItem(item) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemUserProfileSimilarCreatorBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileViewModel.kt new file mode 100644 index 0000000..b7845aa --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileViewModel.kt @@ -0,0 +1,375 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.firebase.dynamiclinks.ShortDynamicLink +import com.google.firebase.dynamiclinks.ktx.androidParameters +import com.google.firebase.dynamiclinks.ktx.dynamicLinks +import com.google.firebase.dynamiclinks.ktx.iosParameters +import com.google.firebase.dynamiclinks.ktx.shortLinkAsync +import com.google.firebase.ktx.Firebase +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.explorer.ExplorerRepository +import kr.co.vividnext.sodalive.report.ReportRepository +import kr.co.vividnext.sodalive.report.ReportRequest +import kr.co.vividnext.sodalive.report.ReportType +import kr.co.vividnext.sodalive.user.UserRepository + +class UserProfileViewModel( + private val repository: ExplorerRepository, + private val reportRepository: ReportRepository, + private val userRepository: UserRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _creatorProfileLiveData = MutableLiveData() + val creatorProfileLiveData: LiveData + get() = _creatorProfileLiveData + + private val _isExpandNotice = MutableLiveData(false) + val isExpandNotice: LiveData + get() = _isExpandNotice + + private var creatorNickname = "" + + fun cheersReport(cheersId: Long, reason: String) { + _isLoading.value = true + + val request = ReportRequest(ReportType.CHEERS, reason, cheersId = cheersId) + compositeDisposable.add( + reportRepository.report( + request = request, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "신고가 접수되었습니다." + ) + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("신고가 접수되었습니다.") + } + ) + ) + } + + fun report(type: ReportType, userId: Long, reason: String = "프로필 신고") { + _isLoading.value = true + + val request = ReportRequest(type, reason, reportedAccountId = userId) + compositeDisposable.add( + reportRepository.report( + request = request, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "신고가 접수되었습니다." + ) + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("신고가 접수되었습니다.") + } + ) + ) + } + + fun getCreatorProfile(userId: Long, onFailure: (() -> Unit)? = null) { + _isLoading.value = true + compositeDisposable.add( + repository.getCreatorProfile( + id = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + val data = it.data + _creatorProfileLiveData.postValue(data!!) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + + if (onFailure != null) { + onFailure() + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + if (onFailure != null) { + onFailure() + } + } + ) + ) + } + + fun follow(creatorId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.creatorFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getCreatorProfile(creatorId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun unFollow(creatorId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.creatorUnFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + getCreatorProfile(creatorId) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun toggleExpandNotice() { + _isExpandNotice.value = !isExpandNotice.value!! + } + + fun writeCheers(parentCheersId: Long? = null, creatorId: Long, cheersContent: String) { + if (cheersContent.isBlank()) { + _toastLiveData.postValue("내용을 입력하세요") + return + } + + _isLoading.value = true + + compositeDisposable.add( + repository.writeCheers( + parentCheersId = parentCheersId, + creatorId = creatorId, + content = cheersContent, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + getCreatorProfile(creatorId) + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun modifyCheers(cheersId: Long, creatorId: Long, cheersContent: String) { + if (cheersContent.isBlank()) { + _toastLiveData.postValue("내용을 입력하세요") + return + } + + _isLoading.value = true + + compositeDisposable.add( + repository.modifyCheers( + cheersId = cheersId, + content = cheersContent, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + getCreatorProfile(creatorId) + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun shareChannel(userId: Long, onSuccess: (String) -> Unit) { + _isLoading.value = true + Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { + link = Uri.parse("https://yozm.day/?channel_id=$userId") + domainUriPrefix = "https://yozm.page.link" + androidParameters { } + iosParameters("kr.co.vividnext.yozm") { + appStoreId = "1630284226" + } + }.addOnSuccessListener { + val uri = it.shortLink + if (uri != null) { + onSuccess("요즘라이브 ${creatorNickname}님의 채널입니다.\n$uri") + } + }.addOnFailureListener { + _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.") + }.addOnCompleteListener { + _isLoading.value = false + } + } + + fun userBlock(userId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.memberBlock( + userId = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + getCreatorProfile(userId) + _toastLiveData.postValue("차단하였습니다.") + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun userUnBlock(userId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.memberUnBlock( + userId = userId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success) { + getCreatorProfile(userId) + _toastLiveData.postValue("차단이 해제 되었습니다.") + } else { + val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PostWriteCheersRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PostWriteCheersRequest.kt new file mode 100644 index 0000000..14453c5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PostWriteCheersRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.explorer.profile.cheers + +import com.google.gson.annotations.SerializedName + +data class PostWriteCheersRequest( + @SerializedName("parentId") val parentId: Long? = null, + @SerializedName("creatorId") val creatorId: Long, + @SerializedName("content") val content: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt new file mode 100644 index 0000000..ec5e11d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer.profile.cheers + +import com.google.gson.annotations.SerializedName + +data class PutModifyCheersRequest( + @SerializedName("cheersId") val cheersId: Long, + @SerializedName("content") val content: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/UserProfileCheersAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/UserProfileCheersAdapter.kt new file mode 100644 index 0000000..2a87151 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/UserProfileCheersAdapter.kt @@ -0,0 +1,126 @@ +package kr.co.vividnext.sodalive.explorer.profile.cheers + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +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.ItemUserProfileCheersBinding +import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponseItem +import kr.co.vividnext.sodalive.extensions.dpToPx + +class UserProfileCheersAdapter( + private val userId: Long, + private val enterReply: (Long, String) -> Unit, + private val modifyReply: (Long, String) -> Unit, + private val onClickReport: (Long) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val context: Context, + private val binding: ItemUserProfileCheersBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(cheers: GetCheersResponseItem) { + binding.tvWriteReply.visibility = View.GONE + binding.llCheerReply.visibility = View.GONE + binding.rlCheerReply.visibility = View.GONE + + binding.ivProfile.load(cheers.profileUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(16.7f.dpToPx())) + } + binding.tvContent.text = cheers.content + binding.tvNickname.text = cheers.nickname + binding.tvDate.text = cheers.date + + binding.ivMenu.setOnClickListener { + showOptionMenu( + context, + binding.ivMenu, + cheersId = cheers.cheersId + ) + } + + if (cheers.replyList.isNotEmpty()) { + binding.tvWriteReply.visibility = View.GONE + binding.llCheerReply.visibility = View.VISIBLE + val reply = cheers.replyList[0] + binding.tvReply.text = reply.content + binding.tvReplyDate.text = reply.date + if (userId == SharedPreferenceManager.userId) { + binding.tvModifyReply.visibility = View.VISIBLE + binding.tvModifyReply.setOnClickListener { + binding.etCheerReply.setText(binding.tvReply.text) + binding.rlCheerReply.visibility = View.VISIBLE + binding.tvSend.setOnClickListener { + val content = binding.etCheerReply.text.toString() + modifyReply(reply.cheersId, content) + } + } + } else { + binding.tvModifyReply.visibility = View.GONE + } + } else { + if (userId == SharedPreferenceManager.userId) { + binding.tvWriteReply.visibility = View.VISIBLE + binding.tvWriteReply.setOnClickListener { + binding.tvWriteReply.visibility = View.GONE + binding.rlCheerReply.visibility = View.VISIBLE + binding.tvSend.setOnClickListener { + val content = binding.etCheerReply.text.toString() + enterReply(cheers.cheersId, content) + } + } + } else { + binding.tvWriteReply.visibility = View.GONE + } + } + + binding.tvReply.requestLayout() + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + parent.context, + ItemUserProfileCheersBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() + + private fun showOptionMenu(context: Context, v: View, cheersId: Long) { + val popup = PopupMenu(context, v) + val inflater = popup.menuInflater + inflater.inflate(R.menu.review_option_menu, popup.menu) + + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_review_report -> { + onClickReport(cheersId) + } + } + + true + } + + popup.show() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAdapter.kt new file mode 100644 index 0000000..404cba3 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAdapter.kt @@ -0,0 +1,76 @@ +package kr.co.vividnext.sodalive.explorer.profile.donation + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemUserProfileDonationBinding +import kr.co.vividnext.sodalive.explorer.profile.UserDonationRankingResponse + +class UserProfileDonationAdapter : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemUserProfileDonationBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: UserDonationRankingResponse, position: Int) { + binding.tvNickname.text = item.nickname + + binding.ivProfile.load(item.profileImage) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + when (position) { + 0 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_1) + binding.ivCrown.visibility = View.VISIBLE + } + + 1 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_2) + binding.ivCrown.visibility = View.VISIBLE + } + + 2 -> { + binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a) + binding.ivBg.visibility = View.VISIBLE + + binding.ivCrown.setImageResource(R.drawable.ic_crown_3) + binding.ivCrown.visibility = View.VISIBLE + } + + else -> { + binding.ivBg.setImageResource(0) + binding.ivBg.visibility = View.GONE + binding.ivCrown.visibility = View.GONE + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemUserProfileDonationBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position], position) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt new file mode 100644 index 0000000..ee23b15 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.explorer.profile.donation + +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.databinding.ActivityUserProfileLiveAllBinding + +class UserProfileDonationAllViewActivity : BaseActivity( + ActivityUserProfileLiveAllBinding::inflate +) { + override fun setupView() {} +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt new file mode 100644 index 0000000..ac9685c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.explorer.profile.fantalk + +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.databinding.ActivityUserProfileFantalkAllBinding + +class UserProfileFantalkAllViewActivity : BaseActivity( + ActivityUserProfileFantalkAllBinding::inflate +) { + override fun setupView() {} +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/GetFollowerListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/GetFollowerListResponse.kt new file mode 100644 index 0000000..c9039a0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/GetFollowerListResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.explorer.profile.follow + +import com.google.gson.annotations.SerializedName + +data class GetFollowerListResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List +) + +data class GetFollowerListResponseItem( + @SerializedName("userId") val userId: Long, + @SerializedName("profileImage") val profileImage: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("isFollow") val isFollow: Boolean? +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListActivity.kt new file mode 100644 index 0000000..31c00d9 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListActivity.kt @@ -0,0 +1,116 @@ +package kr.co.vividnext.sodalive.explorer.profile.follow + +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +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.ActivityUserFollowerListBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import org.koin.android.ext.android.inject + +class UserFollowerListActivity : BaseActivity( + ActivityUserFollowerListBinding::inflate +) { + + private val viewModel: UserFollowerListViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: UserFollowerListAdapter + + private var userId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0) + + if (userId <= 0) { + Toast.makeText( + applicationContext, + "잘못된 요청입니다.\n다시 시도해 주세요.", + Toast.LENGTH_LONG + ).show() + + finish() + } + + super.onCreate(savedInstanceState) + + bindData() + viewModel.getFollowerList() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "팔로워 리스트" + binding.toolbar.tvBack.setOnClickListener { finish() } + + adapter = UserFollowerListAdapter( + onClickRegisterNotification = { viewModel.registerNotification(it) }, + onClickUnRegisterNotification = { viewModel.unRegisterNotification(it) } + ) + + binding.rvFollowerList.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvFollowerList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as? LinearLayoutManager + if ( + layoutManager != null && + layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1 + ) { + viewModel.getFollowerList() + } + } + }) + + binding.rvFollowerList.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.left = 20f.dpToPx().toInt() + outRect.right = 20f.dpToPx().toInt() + } + }) + + binding.rvFollowerList.adapter = adapter + } + + private fun bindData() { + viewModel.userId = userId + viewModel.followerListItemsLiveData.observe(this) { + adapter.addAll(it) + } + + viewModel.totalCountLiveData.observe(this) { + binding.tvCount.text = it.moneyFormat() + } + + 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() + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListAdapter.kt new file mode 100644 index 0000000..efc230e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListAdapter.kt @@ -0,0 +1,75 @@ +package kr.co.vividnext.sodalive.explorer.profile.follow + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemFollowerListBinding + +class UserFollowerListAdapter( + private val onClickRegisterNotification: (Long) -> Unit, + private val onClickUnRegisterNotification: (Long) -> Unit, +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemFollowerListBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetFollowerListResponseItem) { + binding.tvNickname.text = item.nickname + binding.ivProfile.load(item.profileImage) { + transformations(CircleCropTransformation()) + placeholder(R.drawable.bg_placeholder) + crossfade(true) + } + + if (item.isFollow != null) { + binding.ivNotification.visibility = View.VISIBLE + if (item.isFollow) { + binding.ivNotification.setImageResource(R.drawable.btn_notification_selected) + binding.ivNotification.setOnClickListener { + onClickUnRegisterNotification(item.userId) + clear() + } + } else { + binding.ivNotification.setImageResource(R.drawable.btn_notification) + binding.ivNotification.setOnClickListener { + onClickRegisterNotification(item.userId) + clear() + } + } + } else { + binding.ivNotification.visibility = View.GONE + } + } + } + + @SuppressLint("NotifyDataSetChanged") + fun addAll(items: List) { + this.items.addAll(items) + notifyDataSetChanged() + } + + fun clear() { + this.items.clear() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemFollowerListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.size +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListViewModel.kt new file mode 100644 index 0000000..976e796 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/follow/UserFollowerListViewModel.kt @@ -0,0 +1,156 @@ +package kr.co.vividnext.sodalive.explorer.profile.follow + +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.explorer.ExplorerRepository +import kr.co.vividnext.sodalive.user.UserRepository + +class UserFollowerListViewModel( + private val repository: ExplorerRepository, + private val userRepository: UserRepository +) : BaseViewModel() { + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _totalCountLiveData = MutableLiveData(0) + val totalCountLiveData: LiveData + get() = _totalCountLiveData + + private var _followerListItemsLiveData = MutableLiveData>() + val followerListItemsLiveData: LiveData> + get() = _followerListItemsLiveData + + var userId: Long = 0 + var page = 1 + private var isLast = false + private val pageSize = 10 + + fun getFollowerList() { + if (!isLast && !_isLoading.value!!) { + _isLoading.value = true + + compositeDisposable.add( + repository.getFollowerList( + userId, + page, + pageSize, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + val data = it.data + _totalCountLiveData.value = data.totalCount + + if (data.items.isEmpty()) { + isLast = true + } else { + page += 1 + _followerListItemsLiveData.value = data.items + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + ) + ) + } + } + + fun registerNotification(creatorId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.creatorFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + page = 1 + isLast = false + getFollowerList() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun unRegisterNotification(creatorId: Long) { + _isLoading.value = true + compositeDisposable.add( + userRepository.creatorUnFollow( + creatorId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + page = 1 + isLast = false + getFollowerList() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt index 8eb9013..3b4e6d6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt @@ -540,7 +540,7 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl LivePaymentDialog( activity = requireActivity(), layoutInflater = layoutInflater, - title = "${it.price.moneyFormat()}코인으로 예약", + title = "${it.price.moneyFormat()}캔으로 예약", desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.", confirmButtonTitle = "예약하기", confirmButtonClick = { processLiveReservation(roomId) }, @@ -627,7 +627,7 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl LivePaymentDialog( activity = requireActivity(), layoutInflater = layoutInflater, - title = "${it.price.moneyFormat()}코인으로 입장", + title = "${it.price.moneyFormat()}캔으로 입장", desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.", confirmButtonTitle = "결제 후 입장", confirmButtonClick = { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/LiveReservationAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/LiveReservationAdapter.kt index a9e8c58..bb2605e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/LiveReservationAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/reservation/LiveReservationAdapter.kt @@ -99,7 +99,7 @@ class LiveReservationAdapter( binding.tvPrice.text = if (item.price <= 0) { "무료" } else { - "${item.price.moneyFormat()}코인" + "${item.price.moneyFormat()}캔" } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index de2ec4f..d18cfaf 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -344,7 +344,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB if (can > 0) { donation(can, message) } else { - showToast("1코인 이상 후원하실 수 있습니다.") + showToast("1캔 이상 후원하실 수 있습니다.") } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt index acd4fdf..8653a0e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt @@ -527,7 +527,7 @@ class LiveRoomViewModel( _toastLiveData.postValue(it.message) } else { _toastLiveData.postValue( - "후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요." + "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." ) } } @@ -536,7 +536,7 @@ class LiveRoomViewModel( _isLoading.value = false it.message?.let { message -> Logger.e(message) } _toastLiveData.postValue( - "후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요." + "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." ) } ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt index 32c801d..682a0a4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt @@ -259,7 +259,7 @@ data class LiveRoomDonationChat( ) ), 0, - chat.indexOf("코인", 0, true) + 2, + chat.indexOf("캔", 0, true) + 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/status/CanStatusActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/status/CanStatusActivity.kt index 518e317..78d5eac 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/status/CanStatusActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/mypage/can/status/CanStatusActivity.kt @@ -39,7 +39,7 @@ class CanStatusActivity : BaseActivity( } override fun setupView() { - binding.toolbar.tvBack.text = "코인내역" + binding.toolbar.tvBack.text = "캔내역" binding.toolbar.tvBack.setOnClickListener { onClickBackButton() } binding.llChargeCan.setOnClickListener { startActivity( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/CheersReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/CheersReportDialog.kt new file mode 100644 index 0000000..91eca9f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/report/CheersReportDialog.kt @@ -0,0 +1,59 @@ +package kr.co.vividnext.sodalive.report + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.RadioButton +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogReviewReportBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class CheersReportDialog( + activity: Activity, + layoutInflater: LayoutInflater, + confirmButtonClick: (String) -> Unit +) { + + private val alertDialog: AlertDialog + + val dialogView = DialogReviewReportBinding.inflate(layoutInflater) + + var reason = "" + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + dialogView.tvTitle.text = "응원글 신고" + dialogView.tvCancel.setOnClickListener { + alertDialog.dismiss() + } + + dialogView.tvReport.setOnClickListener { + alertDialog.dismiss() + confirmButtonClick(reason) + } + + dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId -> + val radioButton = radioGroup.findViewById(checkedId) + reason = radioButton.text.toString() + } + } + + fun show(width: Int) { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt index 0c1143c..ead11a8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt @@ -46,4 +46,20 @@ class UserRepository(private val userApi: UserApi) { userId: Long, token: String ) = userApi.memberUnBlock(request = MemberBlockRequest(userId), authHeader = token) + + fun creatorFollow( + creatorId: Long, + token: String + ) = userApi.creatorFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) + + fun creatorUnFollow( + creatorId: Long, + token: String + ) = userApi.creatorUnFollow( + request = CreatorFollowRequestRequest(creatorId = creatorId), + authHeader = token + ) } diff --git a/app/src/main/res/drawable-xxhdpi/btn_notification.png b/app/src/main/res/drawable-xxhdpi/btn_notification.png new file mode 100644 index 0000000..4508191 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_notification_selected.png b/app/src/main/res/drawable-xxhdpi/btn_notification_selected.png new file mode 100644 index 0000000..6f17a44 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_notification_selected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_review.png b/app/src/main/res/drawable-xxhdpi/ic_review.png new file mode 100644 index 0000000..2f34ecf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_review.png differ diff --git a/app/src/main/res/drawable/bg_round_corner_10_232323.xml b/app/src/main/res/drawable/bg_round_corner_10_232323.xml new file mode 100644 index 0000000..9698a95 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_232323.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_232323_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_10_232323_9970ff.xml new file mode 100644 index 0000000..17236b0 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_232323_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_4c9970ff.xml b/app/src/main/res/drawable/bg_round_corner_16_7_4c9970ff.xml new file mode 100644 index 0000000..37d2f59 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_16_7_4c9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_16_7_transparent_9970ff.xml new file mode 100644 index 0000000..1a9ea1e --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_16_7_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_transparent_777777.xml b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_777777.xml new file mode 100644 index 0000000..bdea775 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_777777.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_transparent_fdca2f.xml b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_fdca2f.xml new file mode 100644 index 0000000..c318e78 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_fdca2f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_transparent_ff5c49.xml b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_ff5c49.xml new file mode 100644 index 0000000..d3ec7a8 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_3_3_transparent_ff5c49.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_4_7_88909090.xml b/app/src/main/res/drawable/bg_round_corner_4_7_88909090.xml new file mode 100644 index 0000000..7490231 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_4_7_88909090.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_525252.xml b/app/src/main/res/drawable/bg_round_corner_5_3_525252.xml new file mode 100644 index 0000000..cacd58e --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_525252.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_5_3_9970ff.xml new file mode 100644 index 0000000..97a2902 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_dd4500.xml b/app/src/main/res/drawable/bg_round_corner_5_3_dd4500.xml new file mode 100644 index 0000000..94a634e --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_dd4500.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_fdca2f.xml b/app/src/main/res/drawable/bg_round_corner_5_3_fdca2f.xml new file mode 100644 index 0000000..a4b0790 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_fdca2f.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_8_222222_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_8_222222_9970ff.xml new file mode 100644 index 0000000..337bcc2 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_8_222222_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_creator_notice_write.xml b/app/src/main/res/layout/activity_creator_notice_write.xml new file mode 100644 index 0000000..9cae77a --- /dev/null +++ b/app/src/main/res/layout/activity_creator_notice_write.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_follower_list.xml b/app/src/main/res/layout/activity_user_follower_list.xml new file mode 100644 index 0000000..70957d8 --- /dev/null +++ b/app/src/main/res/layout/activity_user_follower_list.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_profile.xml b/app/src/main/res/layout/activity_user_profile.xml index 1354408..68909ae 100644 --- a/app/src/main/res/layout/activity_user_profile.xml +++ b/app/src/main/res/layout/activity_user_profile.xml @@ -1,6 +1,311 @@ - + android:layout_height="match_parent" + android:background="@color/black" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_profile_fantalk_all.xml b/app/src/main/res/layout/activity_user_profile_fantalk_all.xml new file mode 100644 index 0000000..0e786e0 --- /dev/null +++ b/app/src/main/res/layout/activity_user_profile_fantalk_all.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_user_profile_live_all.xml b/app/src/main/res/layout/activity_user_profile_live_all.xml new file mode 100644 index 0000000..1f72cfc --- /dev/null +++ b/app/src/main/res/layout/activity_user_profile_live_all.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_review_report.xml b/app/src/main/res/layout/dialog_review_report.xml new file mode 100644 index 0000000..5376969 --- /dev/null +++ b/app/src/main/res/layout/dialog_review_report.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_follower_list.xml b/app/src/main/res/layout/item_follower_list.xml new file mode 100644 index 0000000..12c2391 --- /dev/null +++ b/app/src/main/res/layout/item_follower_list.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_user_profile_cheers.xml b/app/src/main/res/layout/item_user_profile_cheers.xml new file mode 100644 index 0000000..721c5ed --- /dev/null +++ b/app/src/main/res/layout/item_user_profile_cheers.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_user_profile_donation.xml b/app/src/main/res/layout/item_user_profile_donation.xml new file mode 100644 index 0000000..8b8c851 --- /dev/null +++ b/app/src/main/res/layout/item_user_profile_donation.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_user_profile_donation_all.xml b/app/src/main/res/layout/item_user_profile_donation_all.xml new file mode 100644 index 0000000..e7f4c56 --- /dev/null +++ b/app/src/main/res/layout/item_user_profile_donation_all.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_user_profile_live.xml b/app/src/main/res/layout/item_user_profile_live.xml new file mode 100644 index 0000000..8c2dac2 --- /dev/null +++ b/app/src/main/res/layout/item_user_profile_live.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_user_profile_similar_creator.xml b/app/src/main/res/layout/item_user_profile_similar_creator.xml new file mode 100644 index 0000000..810f099 --- /dev/null +++ b/app/src/main/res/layout/item_user_profile_similar_creator.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile.xml b/app/src/main/res/layout/layout_user_profile.xml new file mode 100644 index 0000000..a938ce6 --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile_donation.xml b/app/src/main/res/layout/layout_user_profile_donation.xml new file mode 100644 index 0000000..3af08fa --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile_donation.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile_fan_talk.xml b/app/src/main/res/layout/layout_user_profile_fan_talk.xml new file mode 100644 index 0000000..0f399af --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile_fan_talk.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile_introduce.xml b/app/src/main/res/layout/layout_user_profile_introduce.xml new file mode 100644 index 0000000..f6dfac1 --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile_introduce.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile_live.xml b/app/src/main/res/layout/layout_user_profile_live.xml new file mode 100644 index 0000000..9159c82 --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile_live.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/layout/layout_user_profile_similar_creator.xml b/app/src/main/res/layout/layout_user_profile_similar_creator.xml new file mode 100644 index 0000000..6ec79bf --- /dev/null +++ b/app/src/main/res/layout/layout_user_profile_similar_creator.xml @@ -0,0 +1,41 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 516b49d..d3c3992 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -67,4 +67,5 @@ #E6A77A #FFB600 #99000000 + #4C9970FF