mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
auto set type image to images with enabled actions
d3b4bff6a684e0771aedfe5bd0ecfcc21f3bdeb7
This commit is contained in:
+7
-2
@@ -11,8 +11,11 @@ import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
|
||||
internal class AccessibilityDelegateWrapper(
|
||||
private val originalDelegate: AccessibilityDelegateCompat?,
|
||||
|
||||
private val initializeAccessibilityNodeInfo:
|
||||
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> }
|
||||
var initializeAccessibilityNodeInfo:
|
||||
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
|
||||
|
||||
var actionsAccessibilityNodeInfo:
|
||||
(host: View?, info: AccessibilityNodeInfoCompat?) -> Unit = { _, _ -> },
|
||||
|
||||
) : AccessibilityDelegateCompat() {
|
||||
|
||||
@@ -52,6 +55,8 @@ internal class AccessibilityDelegateWrapper(
|
||||
?: super.onInitializeAccessibilityNodeInfo(host, info)
|
||||
|
||||
initializeAccessibilityNodeInfo(host, info)
|
||||
|
||||
actionsAccessibilityNodeInfo(host, info)
|
||||
}
|
||||
|
||||
override fun onRequestSendAccessibilityEvent(
|
||||
|
||||
+23
-5
@@ -1,6 +1,7 @@
|
||||
package com.yandex.div.core.view2
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||
import com.yandex.div.core.annotations.Mockable
|
||||
@@ -10,9 +11,12 @@ import com.yandex.div.core.experiments.Experiment.ACCESSIBILITY_ENABLED
|
||||
import com.yandex.div.core.util.AccessibilityStateProvider
|
||||
import com.yandex.div.core.view2.backbutton.BackHandlingRecyclerView
|
||||
import com.yandex.div.core.view2.divs.widgets.DivSliderView
|
||||
import com.yandex.div.json.expressions.ExpressionResolver
|
||||
import com.yandex.div2.DivAccessibility
|
||||
import com.yandex.div2.DivBase
|
||||
import com.yandex.div2.DivContainer
|
||||
import com.yandex.div2.DivGallery
|
||||
import com.yandex.div2.DivGifImage
|
||||
import com.yandex.div2.DivImage
|
||||
import com.yandex.div2.DivInput
|
||||
import com.yandex.div2.DivSelect
|
||||
@@ -56,17 +60,23 @@ internal class DivAccessibilityBinder @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun bindType(view: View, divBase: DivBase, type: DivAccessibility.Type) {
|
||||
fun bindType(view: View, divBase: DivBase, type: DivAccessibility.Type, resolver: ExpressionResolver) {
|
||||
if (!accessibilityStateProvider.isAccessibilityEnabled(view.context)) {
|
||||
return
|
||||
}
|
||||
|
||||
val originalDelegate = ViewCompat.getAccessibilityDelegate(view)
|
||||
val accessibilityType = type.toAccessibilityType(divBase)
|
||||
val accessibilityType = type.toAccessibilityType(divBase, resolver)
|
||||
|
||||
val accessibilityDelegate =
|
||||
if (accessibilityType == AccessibilityType.LIST && view is BackHandlingRecyclerView) {
|
||||
AccessibilityListDelegate(view)
|
||||
} else if (originalDelegate is AccessibilityDelegateWrapper) {
|
||||
originalDelegate.apply {
|
||||
initializeAccessibilityNodeInfo = { _, info ->
|
||||
info?.bindType(accessibilityType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AccessibilityDelegateWrapper(
|
||||
originalDelegate,
|
||||
@@ -168,15 +178,23 @@ internal class DivAccessibilityBinder @Inject constructor(
|
||||
isFocusable = actionable
|
||||
}
|
||||
|
||||
private fun DivAccessibility.Type.toAccessibilityType(div: DivBase): AccessibilityType =
|
||||
when (this) {
|
||||
private fun DivImage.isClickable(resolver: ExpressionResolver): Boolean =
|
||||
if (action != null && action?.isEnabled?.evaluate(resolver) == true) {
|
||||
true
|
||||
} else if (actions != null && actions?.any { it.isEnabled.evaluate(resolver) } == true) {
|
||||
true
|
||||
} else longtapActions != null && longtapActions?.any { it.isEnabled.evaluate(resolver) } == true
|
||||
|
||||
private fun DivAccessibility.Type.toAccessibilityType(
|
||||
div: DivBase, resolver: ExpressionResolver
|
||||
): AccessibilityType = when (this) {
|
||||
DivAccessibility.Type.AUTO -> when {
|
||||
div is DivInput -> AccessibilityType.EDIT_TEXT
|
||||
div is DivText -> AccessibilityType.TEXT
|
||||
div is DivTabs -> AccessibilityType.TAB_WIDGET
|
||||
div is DivSelect -> AccessibilityType.SELECT
|
||||
div is DivSlider -> AccessibilityType.SLIDER
|
||||
div is DivImage && div.accessibility != null -> AccessibilityType.IMAGE
|
||||
div is DivImage && (div.accessibility != null || div.isClickable(resolver))-> AccessibilityType.IMAGE
|
||||
div is DivGallery && div.accessibility?.description != null -> AccessibilityType.PAGER
|
||||
else -> AccessibilityType.NONE
|
||||
}
|
||||
|
||||
+5
-2
@@ -50,6 +50,7 @@ import com.yandex.div.json.expressions.ExpressionResolver
|
||||
import com.yandex.div.json.expressions.equalsToConstant
|
||||
import com.yandex.div.json.expressions.isConstantOrNull
|
||||
import com.yandex.div2.Div
|
||||
import com.yandex.div2.DivAccessibility
|
||||
import com.yandex.div2.DivAction
|
||||
import com.yandex.div2.DivAlignmentHorizontal
|
||||
import com.yandex.div2.DivAlignmentVertical
|
||||
@@ -511,7 +512,8 @@ internal fun View.applyDivActions(
|
||||
actions: List<DivAction>?,
|
||||
longTapActions: List<DivAction>?,
|
||||
doubleTapActions: List<DivAction>?,
|
||||
actionAnimation: DivAnimation
|
||||
actionAnimation: DivAnimation,
|
||||
accessibility: DivAccessibility?,
|
||||
) {
|
||||
val actionBinder = divView.div2Component.actionBinder
|
||||
val tapActions = if (actions.isNullOrEmpty()) {
|
||||
@@ -519,7 +521,8 @@ internal fun View.applyDivActions(
|
||||
} else {
|
||||
actions
|
||||
}
|
||||
actionBinder.bindDivActions(divView, this, tapActions, longTapActions, doubleTapActions, actionAnimation)
|
||||
actionBinder.bindDivActions(divView, this, tapActions, longTapActions, doubleTapActions,
|
||||
actionAnimation, accessibility)
|
||||
}
|
||||
|
||||
internal fun View.setAnimatedTouchListener(
|
||||
|
||||
+50
-29
@@ -3,9 +3,9 @@ package com.yandex.div.core.view2.divs
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.StringDef
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.AccessibilityDelegateCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||
import com.yandex.div.R
|
||||
@@ -20,11 +20,8 @@ import com.yandex.div.core.experiments.Experiment.ACCESSIBILITY_ENABLED
|
||||
import com.yandex.div.core.experiments.Experiment.IGNORE_ACTION_MENU_ITEMS_ENABLED
|
||||
import com.yandex.div.core.experiments.Experiment.LONGTAP_ACTIONS_PASS_TO_CHILD_ENABLED
|
||||
import com.yandex.div.core.view2.AccessibilityDelegateWrapper
|
||||
import com.yandex.div.core.view2.AccessibilityListDelegate
|
||||
import com.yandex.div.core.view2.Div2View
|
||||
import com.yandex.div.core.view2.DivAccessibilityBinder
|
||||
import com.yandex.div.core.view2.DivGestureListener
|
||||
import com.yandex.div.core.view2.backbutton.BackHandlingRecyclerView
|
||||
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_BLUR
|
||||
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_CLICK
|
||||
import com.yandex.div.core.view2.divs.DivActionBinder.LogType.Companion.LOG_DOUBLE_CLICK
|
||||
@@ -70,10 +67,9 @@ internal class DivActionBinder @Inject constructor(
|
||||
actions: List<DivAction>?,
|
||||
longTapActions: List<DivAction>?,
|
||||
doubleTapActions: List<DivAction>?,
|
||||
actionAnimation: DivAnimation
|
||||
actionAnimation: DivAnimation,
|
||||
accessibility: DivAccessibility?,
|
||||
) {
|
||||
val originalDelegate = ViewCompat.getAccessibilityDelegate(target)
|
||||
|
||||
val onApply = {
|
||||
applyDivActions(
|
||||
divView = divView,
|
||||
@@ -82,7 +78,7 @@ internal class DivActionBinder @Inject constructor(
|
||||
doubleTapActions = doubleTapActions.onlyEnabled(divView.expressionResolver),
|
||||
longTapActions = longTapActions.onlyEnabled(divView.expressionResolver),
|
||||
actionAnimation = actionAnimation,
|
||||
originalDelegate = originalDelegate,
|
||||
accessibility = accessibility,
|
||||
)
|
||||
}
|
||||
val resolver = divView.expressionResolver
|
||||
@@ -101,7 +97,7 @@ internal class DivActionBinder @Inject constructor(
|
||||
longTapActions: List<DivAction>,
|
||||
doubleTapActions: List<DivAction>,
|
||||
actionAnimation: DivAnimation,
|
||||
originalDelegate: AccessibilityDelegateCompat?,
|
||||
accessibility: DivAccessibility?,
|
||||
) {
|
||||
val clickableState = target.isClickable
|
||||
val longClickableState = target.isLongClickable
|
||||
@@ -120,30 +116,55 @@ internal class DivActionBinder @Inject constructor(
|
||||
divGestureListener
|
||||
)
|
||||
|
||||
if (accessibilityEnabled &&
|
||||
DivAccessibility.Mode.MERGE == divView.getPropagatedAccessibilityMode(target) &&
|
||||
divView.isDescendantAccessibilityMode(target)
|
||||
) {
|
||||
target.isClickable = clickableState
|
||||
target.isLongClickable = longClickableState
|
||||
if (accessibilityEnabled) {
|
||||
if (DivAccessibility.Mode.MERGE == divView.getPropagatedAccessibilityMode(target) &&
|
||||
divView.isDescendantAccessibilityMode(target)) {
|
||||
target.isClickable = clickableState
|
||||
target.isLongClickable = longClickableState
|
||||
}
|
||||
|
||||
val accessibilityWrapper = AccessibilityDelegateWrapper(
|
||||
originalDelegate,
|
||||
initializeAccessibilityNodeInfo = { _, info ->
|
||||
if (actions.isNotEmpty()) {
|
||||
info?.addAction(AccessibilityNodeInfoCompat
|
||||
.AccessibilityActionCompat.ACTION_CLICK)
|
||||
}
|
||||
if (longTapActions.isNotEmpty()) {
|
||||
info?.addAction(AccessibilityNodeInfoCompat
|
||||
.AccessibilityActionCompat.ACTION_LONG_CLICK)
|
||||
}
|
||||
})
|
||||
|
||||
ViewCompat.setAccessibilityDelegate(target, accessibilityWrapper)
|
||||
bindAccessibilityDelegate(target, actions, longTapActions, accessibility)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAccessibilityDelegate(
|
||||
target: View,
|
||||
actions: List<DivAction>,
|
||||
longTapActions: List<DivAction>,
|
||||
accessibility: DivAccessibility?,
|
||||
) {
|
||||
val originalDelegate = ViewCompat.getAccessibilityDelegate(target)
|
||||
|
||||
val action = { _: View?, info: AccessibilityNodeInfoCompat? ->
|
||||
if (actions.isNotEmpty()) {
|
||||
info?.addAction(AccessibilityNodeInfoCompat
|
||||
.AccessibilityActionCompat.ACTION_CLICK)
|
||||
}
|
||||
if (longTapActions.isNotEmpty()) {
|
||||
info?.addAction(AccessibilityNodeInfoCompat
|
||||
.AccessibilityActionCompat.ACTION_LONG_CLICK)
|
||||
}
|
||||
if (target is ImageView && (accessibility?.type == DivAccessibility.Type.AUTO || accessibility == null)) {
|
||||
if (longTapActions.isNotEmpty() || actions.isNotEmpty() || accessibility?.description != null) {
|
||||
info?.className = "android.widget.ImageView"
|
||||
} else {
|
||||
info?.className = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val accessibilityWrapper = if (originalDelegate is AccessibilityDelegateWrapper) {
|
||||
originalDelegate.actionsAccessibilityNodeInfo = action
|
||||
originalDelegate
|
||||
} else {
|
||||
AccessibilityDelegateWrapper(
|
||||
originalDelegate,
|
||||
actionsAccessibilityNodeInfo = action)
|
||||
}
|
||||
|
||||
ViewCompat.setAccessibilityDelegate(target, accessibilityWrapper)
|
||||
}
|
||||
|
||||
private fun bindTapActions(
|
||||
divView: Div2View,
|
||||
target: View,
|
||||
|
||||
@@ -273,11 +273,11 @@ internal class DivBaseBinder @Inject constructor(
|
||||
if (newDiv.accessibility == null && oldDiv?.accessibility == null) {
|
||||
// Shortcut for empty accessibility binding
|
||||
applyAccessibilityMode(divView, newDiv, mode = null)
|
||||
divAccessibilityBinder.bindType(this, newDiv, DivAccessibility.Type.AUTO)
|
||||
divAccessibilityBinder.bindType(this, newDiv, DivAccessibility.Type.AUTO, resolver)
|
||||
return
|
||||
}
|
||||
|
||||
bindAccessibilityType(newDiv, oldDiv)
|
||||
bindAccessibilityType(newDiv, oldDiv, resolver)
|
||||
bindAccessibilityDescriptionAndHint(newDiv, oldDiv, resolver, subscriber)
|
||||
bindAccessibilityMode(divView, newDiv, resolver, subscriber)
|
||||
bindAccessibilityStateDescription(newDiv, oldDiv, resolver, subscriber)
|
||||
@@ -286,14 +286,15 @@ internal class DivBaseBinder @Inject constructor(
|
||||
|
||||
private fun View.bindAccessibilityType(
|
||||
newDiv: DivBase,
|
||||
oldDiv: DivBase?
|
||||
oldDiv: DivBase?,
|
||||
resolver: ExpressionResolver,
|
||||
) {
|
||||
if (oldDiv != null && newDiv.accessibility?.type == oldDiv.accessibility?.type) {
|
||||
return
|
||||
}
|
||||
|
||||
divAccessibilityBinder.bindType(this, newDiv, newDiv.accessibility?.type
|
||||
?: DivAccessibility.Type.AUTO)
|
||||
?: DivAccessibility.Type.AUTO, resolver)
|
||||
}
|
||||
|
||||
private fun View.bindAccessibilityDescriptionAndHint(
|
||||
|
||||
+2
-1
@@ -75,7 +75,8 @@ internal class DivContainerBinder @Inject constructor(
|
||||
val oldDiv = divHolderView.div
|
||||
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
val resolver = divView.expressionResolver
|
||||
val oldResolver = divView.oldExpressionResolver
|
||||
|
||||
+2
-1
@@ -52,7 +52,8 @@ internal class DivGifImageBinder @Inject constructor(
|
||||
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
view.bindAspectRatio(div.aspect, oldDiv?.aspect, expressionResolver)
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ internal class DivGridBinder @Inject constructor(
|
||||
view.releaseViewVisitor = divView.releaseViewVisitor
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
view.addSubscription(
|
||||
div.columnCount.observeAndGet(resolver) { columnCount -> view.columnCount = columnCount.toIntSafely() }
|
||||
|
||||
@@ -42,7 +42,8 @@ internal class DivImageBinder @Inject constructor(
|
||||
if (div === oldDiv) return
|
||||
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
val expressionResolver = divView.expressionResolver
|
||||
val errorCollector = errorCollectors.getOrCreate(divView.dataTag, divView.divData)
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.core.view.doOnLayout
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import com.yandex.div.core.dagger.DivScope
|
||||
import com.yandex.div.core.expression.variables.TwoWayStringVariableBinder
|
||||
import com.yandex.div.core.util.AccessibilityStateProvider
|
||||
import com.yandex.div.core.util.expressionSubscriber
|
||||
import com.yandex.div.core.util.mask.BaseInputMask
|
||||
import com.yandex.div.core.util.mask.CurrencyInputMask
|
||||
@@ -47,6 +48,7 @@ internal class DivInputBinder @Inject constructor(
|
||||
private val baseBinder: DivBaseBinder,
|
||||
private val typefaceResolver: DivTypefaceResolver,
|
||||
private val variableBinder: TwoWayStringVariableBinder,
|
||||
private val accessibilityStateProvider: AccessibilityStateProvider,
|
||||
private val errorCollectors: ErrorCollectors
|
||||
) : DivViewBinder<DivInput, DivInputView> {
|
||||
|
||||
@@ -62,6 +64,7 @@ internal class DivInputBinder @Inject constructor(
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
|
||||
accessibilityEnabled = accessibilityStateProvider.isAccessibilityEnabled(view.context)
|
||||
|
||||
observeBackground(divView, div, oldDiv, expressionResolver)
|
||||
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ internal class DivSeparatorBinder @Inject constructor(
|
||||
if (div === oldDiv) return
|
||||
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
view.bindStyle(div.delimiterStyle, oldDiv?.delimiterStyle, divView.expressionResolver)
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ internal class DivTextBinder @Inject constructor(
|
||||
if (div === oldDiv) return
|
||||
|
||||
baseBinder.bindView(view, div, oldDiv, divView)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions, div.doubletapActions, div.actionAnimation)
|
||||
view.applyDivActions(divView, div.action, div.actions, div.longtapActions,
|
||||
div.doubletapActions, div.actionAnimation, div.accessibility)
|
||||
|
||||
val expressionResolver = divView.expressionResolver
|
||||
view.bindTypeface(div, oldDiv, expressionResolver)
|
||||
|
||||
+7
-4
@@ -26,11 +26,14 @@ internal class DivInputView @JvmOverloads constructor(
|
||||
|
||||
private var textChangeWatcher: TextWatcher? = null
|
||||
|
||||
private val accessibilityManager = context.getSystemService(
|
||||
Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
|
||||
|
||||
private var _hint: String? = null
|
||||
|
||||
internal var accessibilityEnabled: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
setInputHint(_hint)
|
||||
}
|
||||
|
||||
init {
|
||||
setPadding(0, 0, 0, 0)
|
||||
}
|
||||
@@ -38,7 +41,7 @@ internal class DivInputView @JvmOverloads constructor(
|
||||
fun setInputHint(hint: String?) {
|
||||
_hint = hint
|
||||
this.hint = when {
|
||||
accessibilityManager?.isTouchExplorationEnabled != true -> hint
|
||||
!accessibilityEnabled -> hint
|
||||
hint.isNullOrEmpty() && contentDescription.isNullOrEmpty() -> null
|
||||
hint.isNullOrEmpty() -> contentDescription
|
||||
contentDescription.isNullOrEmpty() -> hint
|
||||
|
||||
@@ -21,6 +21,7 @@ internal fun assertActionApplied(divView: Div2View, target: View, actionUri: Uri
|
||||
longTapActions = anyOrNull(),
|
||||
doubleTapActions = anyOrNull(),
|
||||
actionAnimation = any(),
|
||||
accessibility = anyOrNull(),
|
||||
)
|
||||
|
||||
val action = actionCaptor.firstValue.find {
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.robolectric.RuntimeEnvironment
|
||||
open class DivBinderTest {
|
||||
|
||||
internal val actionBinder = mock<DivActionBinder> {
|
||||
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any()) }.thenCallRealMethod()
|
||||
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull()) }.thenCallRealMethod()
|
||||
}
|
||||
private val mockComponent = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
|
||||
on { actionBinder } doReturn actionBinder
|
||||
|
||||
@@ -37,7 +37,7 @@ internal fun divView(
|
||||
)
|
||||
|
||||
val actionBinder = mock<DivActionBinder> {
|
||||
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any()) }.thenCallRealMethod()
|
||||
on { bindDivActions(any(), any(), anyOrNull(), anyOrNull(), anyOrNull(), any(), anyOrNull()) }.thenCallRealMethod()
|
||||
}
|
||||
|
||||
val component = mock<Div2Component>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
|
||||
|
||||
Reference in New Issue
Block a user