AudioContent 상세 문자열 리소스화
상세 화면과 다이얼로그의 하드코딩 텍스트를 문자열 리소스로 이동했습니다. ko/en/ja 리소스 추가 및 기본 시간 표시, 토스트, 공유 문구를 리소스 키로 통일했습니다.
This commit is contained in:
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
@@ -31,7 +32,10 @@ class AudioContentDeleteDialog(
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?"
|
||||
dialogView.tvTitle.text = activity.getString(
|
||||
R.string.screen_audio_content_detail_delete_confirm_message,
|
||||
title
|
||||
)
|
||||
dialogView.tvCancel.setOnClickListener {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
@@ -43,7 +47,9 @@ class AudioContentDeleteDialog(
|
||||
} else {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
"동의하셔야 삭제할 수 있습니다.",
|
||||
activity.getString(
|
||||
R.string.screen_audio_content_detail_delete_consent_required
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -94,8 +94,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
binding.sbProgress.progress = 0
|
||||
binding.ivPlayOrPause.setImageResource(0)
|
||||
binding.tvTotalDuration.text = " / 00:00:00"
|
||||
binding.tvCurrentDuration.text = "00:00:00"
|
||||
binding.tvTotalDuration.text = getString(
|
||||
R.string.screen_audio_content_detail_time_total_default
|
||||
)
|
||||
binding.tvCurrentDuration.text =
|
||||
getString(R.string.screen_audio_content_detail_time_default)
|
||||
binding.rlPreviewAlert.visibility = View.GONE
|
||||
|
||||
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
@@ -109,7 +112,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
if (audioContentId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.screen_audio_content_error_invalid_request),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -148,7 +155,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.tvBack.text = "콘텐츠 상세"
|
||||
binding.tvBack.text = getString(R.string.screen_audio_content_detail_title)
|
||||
binding.tvBack.setOnClickListener { finish() }
|
||||
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
|
||||
binding.ivMenu.setOnClickListener {
|
||||
@@ -292,9 +299,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
LayoutInflater.from(this)
|
||||
) { can, message, _ ->
|
||||
if (can <= 0) {
|
||||
showToast("1캔 이상 후원하실 수 있습니다.")
|
||||
showToast(getString(R.string.screen_audio_content_detail_donation_minimum))
|
||||
} else if (message.isBlank()) {
|
||||
showToast("함께 보낼 메시지를 입력하세요.")
|
||||
showToast(
|
||||
getString(R.string.screen_audio_content_detail_donation_empty_message)
|
||||
)
|
||||
} else {
|
||||
donation(can, message)
|
||||
}
|
||||
@@ -355,13 +364,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
SodaDialog(
|
||||
this@AudioContentDetailActivity,
|
||||
layoutInflater,
|
||||
"고정 한도 도달",
|
||||
"이 콘텐츠를 고정하시겠어요? " +
|
||||
"채널에 콘텐츠를 최대 3개까지 고정할 수 있습니다." +
|
||||
"이 콘텐츠를 고정하면 가장 오래된 콘텐츠가 대체됩니다.",
|
||||
"확인",
|
||||
getString(R.string.screen_audio_content_detail_pin_limit_title),
|
||||
getString(R.string.screen_audio_content_detail_pin_limit_message),
|
||||
getString(R.string.confirm),
|
||||
{ viewModel.pinContent(audioContentId) },
|
||||
"취소",
|
||||
getString(R.string.cancel),
|
||||
{}
|
||||
).show(screenWidth)
|
||||
}
|
||||
@@ -694,28 +701,31 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
}
|
||||
|
||||
binding.tvUnit.text = if (SharedPreferenceManager.userId == 17958L) {
|
||||
"원으로"
|
||||
getString(R.string.screen_audio_content_detail_purchase_unit_won)
|
||||
} else {
|
||||
"캔으로"
|
||||
getString(R.string.screen_audio_content_detail_purchase_unit_can)
|
||||
}
|
||||
|
||||
when (response.purchaseOption) {
|
||||
PurchaseOption.BOTH -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 구매하기"
|
||||
binding.tvStrPurchaseOrRental.text =
|
||||
getString(R.string.screen_audio_content_detail_action_buy)
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_3bb9f1
|
||||
)
|
||||
}
|
||||
|
||||
PurchaseOption.BUY_ONLY -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 소장하기"
|
||||
binding.tvStrPurchaseOrRental.text =
|
||||
getString(R.string.screen_audio_content_detail_action_keep)
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_59548f
|
||||
)
|
||||
}
|
||||
|
||||
PurchaseOption.RENT_ONLY -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 대여하기"
|
||||
binding.tvStrPurchaseOrRental.text =
|
||||
getString(R.string.screen_audio_content_detail_action_rental)
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_548f7d
|
||||
)
|
||||
@@ -757,7 +767,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.ivSeekBackward10.visibility = View.GONE
|
||||
binding.ivSeekForward10.visibility = View.GONE
|
||||
binding.tvPreviewNo.visibility = View.GONE
|
||||
binding.tvTotalDuration.text = " / ${response.duration}"
|
||||
binding.tvTotalDuration.text = getString(
|
||||
R.string.screen_audio_content_detail_total_duration_format,
|
||||
response.duration
|
||||
)
|
||||
|
||||
isAlertPreview = response.creator.creatorId != SharedPreferenceManager.userId &&
|
||||
!response.existOrdered &&
|
||||
@@ -941,7 +954,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TEXT, it)
|
||||
|
||||
val shareIntent = Intent.createChooser(intent, "오디오 콘텐츠 공유")
|
||||
val shareIntent = Intent.createChooser(
|
||||
intent,
|
||||
getString(R.string.screen_audio_content_detail_share_title)
|
||||
)
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
@@ -1219,7 +1235,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
if (duration != null && duration > 0) {
|
||||
binding.sbProgress.max = duration
|
||||
binding.tvTotalDuration.text = " / ${Utils.convertDurationToString(duration)}"
|
||||
binding.tvTotalDuration.text = getString(
|
||||
R.string.screen_audio_content_detail_total_duration_format,
|
||||
Utils.convertDurationToString(duration)
|
||||
)
|
||||
}
|
||||
|
||||
if (progress != null && progress > 0) {
|
||||
|
||||
@@ -36,10 +36,11 @@ class AudioContentDetailMenuBottomSheetDialog(
|
||||
|
||||
if (isPin) {
|
||||
dialog.ivPin.setImageResource(R.drawable.ic_pin_cancel)
|
||||
dialog.tvPin.text = "내 채널에 고정 취소"
|
||||
dialog.tvPin.text =
|
||||
getString(R.string.screen_audio_content_detail_pin_cancel)
|
||||
} else {
|
||||
dialog.ivPin.setImageResource(R.drawable.ic_pin)
|
||||
dialog.tvPin.text = "내 채널에 고정"
|
||||
dialog.tvPin.text = getString(R.string.screen_audio_content_detail_pin)
|
||||
}
|
||||
|
||||
dialog.llPin.setOnClickListener {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
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.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
||||
import kr.co.vividnext.sodalive.common.Utils
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
|
||||
@@ -52,6 +55,9 @@ class AudioContentDetailViewModel(
|
||||
val isContentPlayLoopLiveData: LiveData<Boolean>
|
||||
get() = _isContentPlayLoopLiveData
|
||||
|
||||
private fun getString(@StringRes resId: Int, vararg args: Any) =
|
||||
SodaLiveApplicationHolder.get().getString(resId, *args)
|
||||
|
||||
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
|
||||
if (!isLoading.value!!) {
|
||||
isLoading.value = true
|
||||
@@ -73,7 +79,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,7 +93,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
if (onFailure != null) {
|
||||
onFailure()
|
||||
}
|
||||
@@ -116,7 +122,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,7 +131,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -155,9 +161,13 @@ class AudioContentDetailViewModel(
|
||||
getAudioContentDetail(audioContentId = contentId)
|
||||
_toastLiveData.postValue(
|
||||
if (orderType == OrderType.RENTAL) {
|
||||
"대여가 완료되었습니다."
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_order_rental_success
|
||||
)
|
||||
} else {
|
||||
"구매가 완료되었습니다."
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_order_keep_success
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -168,7 +178,7 @@ class AudioContentDetailViewModel(
|
||||
}
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +187,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -201,7 +211,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -211,7 +221,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -240,7 +250,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -250,7 +260,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -283,7 +293,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -291,7 +301,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -327,7 +337,7 @@ class AudioContentDetailViewModel(
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue(
|
||||
"삭제되었습니다."
|
||||
getString(R.string.screen_audio_content_detail_delete_success)
|
||||
)
|
||||
onSuccess()
|
||||
} else {
|
||||
@@ -335,7 +345,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -343,7 +353,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -365,7 +375,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"신고가 접수되었습니다."
|
||||
getString(R.string.screen_audio_content_detail_report_submitted)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -374,7 +384,9 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("신고가 접수되었습니다.")
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_report_submitted)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -404,7 +416,10 @@ class AudioContentDetailViewModel(
|
||||
if (it.success) {
|
||||
SharedPreferenceManager.can -= can
|
||||
_toastLiveData.postValue(
|
||||
"${can.moneyFormat()}캔을 후원하였습니다."
|
||||
getString(
|
||||
R.string.screen_audio_content_detail_donation_success,
|
||||
can.moneyFormat()
|
||||
)
|
||||
)
|
||||
onSuccess()
|
||||
} else {
|
||||
@@ -412,7 +427,7 @@ class AudioContentDetailViewModel(
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -420,7 +435,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -440,14 +455,16 @@ class AudioContentDetailViewModel(
|
||||
isLoading.value = false
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue("고정되었습니다.")
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_pin_success)
|
||||
)
|
||||
getAudioContentDetail(audioContentId)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -455,7 +472,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -475,14 +492,16 @@ class AudioContentDetailViewModel(
|
||||
isLoading.value = false
|
||||
|
||||
if (it.success) {
|
||||
_toastLiveData.postValue("해제되었습니다.")
|
||||
_toastLiveData.postValue(
|
||||
getString(R.string.screen_audio_content_detail_unpin_success)
|
||||
)
|
||||
getAudioContentDetail(audioContentId)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
getString(R.string.common_error_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -490,7 +509,7 @@ class AudioContentDetailViewModel(
|
||||
{
|
||||
isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
_toastLiveData.postValue(getString(R.string.common_error_unknown))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.WindowManager
|
||||
import android.widget.RadioButton
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentReportBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
@@ -37,7 +38,13 @@ class AudioContentReportDialog(
|
||||
alertDialog.dismiss()
|
||||
confirmButtonClick(reason)
|
||||
} else {
|
||||
Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
activity,
|
||||
activity.getString(
|
||||
R.string.screen_audio_content_detail_report_reason_required
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user