mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 09:11:52 +01:00
Add joystick press and general clean up
This commit is contained in:
parent
3057e4b29a
commit
e023dbbf0a
@ -411,9 +411,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) {
|
||||
setButtonState(0, buttonId.value(), state.state)
|
||||
}
|
||||
private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) = setButtonState(0, buttonId.value(), state.state)
|
||||
|
||||
private fun onStickStateChanged(buttonId : ButtonId, position : PointF) {
|
||||
val stickId = when (buttonId) {
|
||||
@ -423,7 +421,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
|
||||
else -> error("Invalid button id")
|
||||
}
|
||||
Log.i("blaa", "$position")
|
||||
setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt())
|
||||
setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted
|
||||
}
|
||||
|
@ -49,9 +49,7 @@ class MainActivity : AppCompatActivity() {
|
||||
/**
|
||||
* The adapter used for adding elements to [app_list]
|
||||
*/
|
||||
private val adapter by lazy {
|
||||
AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||
}
|
||||
private lateinit var adapter : AppAdapter
|
||||
|
||||
private var reloading = AtomicBoolean()
|
||||
|
||||
@ -223,6 +221,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val metrics = resources.displayMetrics
|
||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||
app_list.adapter = adapter
|
||||
app_list.layoutManager = when (adapter.layoutType) {
|
||||
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||
|
@ -15,12 +15,13 @@ import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.app_item_grid_compact.*
|
||||
|
||||
/**
|
||||
* This enumerates the type of layouts the menu can be in
|
||||
@ -47,23 +48,9 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param icon The ImageView associated with the icon
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
*/
|
||||
private class ItemViewHolder(val parent : View, var icon : ImageView, var title : TextView, var subtitle : TextView, var card : View? = null) : RecyclerView.ViewHolder(parent)
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent : View, var header : TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
@ -79,20 +66,9 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
|
||||
}
|
||||
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)).apply {
|
||||
if (layoutType == LayoutType.Grid || layoutType == LayoutType.GridCompact) {
|
||||
card = view.findViewById(R.id.app_item_grid)
|
||||
title.isSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
ElementType.Item -> ItemViewHolder(view)
|
||||
|
||||
ElementType.Header -> {
|
||||
HeaderViewHolder(view).apply {
|
||||
header = view.findViewById(R.id.text_title)
|
||||
}
|
||||
}
|
||||
ElementType.Header -> HeaderViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,8 +79,8 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is AppItem && holder is ItemViewHolder) {
|
||||
holder.title.text = item.title
|
||||
holder.subtitle.text = item.subTitle ?: item.loaderResultString(holder.subtitle.context)
|
||||
holder.text_title.text = item.title
|
||||
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
@ -114,13 +90,13 @@ internal class AppAdapter(val layoutType : LayoutType, private val onClick : Int
|
||||
|
||||
when (layoutType) {
|
||||
LayoutType.List -> holder.itemView
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card!!
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
|
||||
}.apply {
|
||||
setOnClickListener { onClick.invoke(item) }
|
||||
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
|
||||
}
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header!!.text = item.title
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,13 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.controller_item.*
|
||||
import kotlinx.android.synthetic.main.section_item.text_title
|
||||
|
||||
/**
|
||||
* This is a class that holds everything relevant to a single item in the controller configuration list
|
||||
@ -22,14 +24,8 @@ import emu.skyline.input.*
|
||||
* @param subContent The secondary line of text to show data more specific data about the item
|
||||
*/
|
||||
abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() {
|
||||
/**
|
||||
* The underlying adapter this item is contained within
|
||||
*/
|
||||
var adapter : ControllerAdapter? = null
|
||||
lateinit var adapter : ControllerAdapter
|
||||
|
||||
/**
|
||||
* The position of this item in the adapter
|
||||
*/
|
||||
var position : Int? = null
|
||||
|
||||
/**
|
||||
@ -42,7 +38,7 @@ abstract class ControllerItem(var content : String, var subContent : String) : B
|
||||
if (subContent != null)
|
||||
this.subContent = subContent
|
||||
|
||||
position?.let { adapter?.notifyItemChanged(it) }
|
||||
position?.let { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,71 +145,45 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
|
||||
override fun update() = update(null, getSummary(context, stick))
|
||||
}
|
||||
|
||||
class ControllerCheckBox()
|
||||
|
||||
/**
|
||||
* This adapter is used to create a list which handles having a simple view
|
||||
*/
|
||||
class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
*/
|
||||
fun addHeader(string : String) {
|
||||
super.addHeader(BaseHeader(string))
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions sets [ControllerItem.adapter] and delegates the call to [HeaderAdapter.addItem]
|
||||
*/
|
||||
fun addItem(item : ControllerItem) {
|
||||
item.adapter = this
|
||||
super.addItem(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
* @param item The View containing the two other views
|
||||
*/
|
||||
class ItemViewHolder(val parent : View, var title : TextView, var subtitle : TextView, var item : View) : RecyclerView.ViewHolder(parent)
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent : View, var header : TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = when (ElementType.values()[viewType]) {
|
||||
ElementType.Header -> LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false).let { view ->
|
||||
HeaderViewHolder(view).apply { header = view.findViewById(R.id.text_title) }
|
||||
}
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder = LayoutInflater.from(parent.context).let { layoutInflater ->
|
||||
when (ElementType.values()[viewType]) {
|
||||
ElementType.Header -> HeaderViewHolder(layoutInflater.inflate(R.layout.section_item, parent, false))
|
||||
|
||||
ElementType.Item -> LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false).let { view ->
|
||||
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
|
||||
ElementType.Item -> ItemViewHolder(layoutInflater.inflate(R.layout.controller_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function binds the item at [position] to the supplied [holder]
|
||||
*/
|
||||
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is ControllerItem && holder is ItemViewHolder) {
|
||||
item.position = position
|
||||
|
||||
holder.title.text = item.content
|
||||
holder.subtitle.text = item.subContent
|
||||
holder.text_title.text = item.content
|
||||
holder.text_subtitle.text = item.subContent
|
||||
|
||||
holder.parent.setOnClickListener { onItemClickCallback.invoke(item) }
|
||||
holder.itemView.setOnClickListener { onItemClickCallback.invoke(item) }
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header?.text = item.title
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,12 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.BaseItem
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.log_item.*
|
||||
|
||||
/**
|
||||
* This class is used to hold all data about a log entry
|
||||
@ -55,22 +56,9 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The ViewHolder used by items is used to hold the views associated with an item
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param title The TextView associated with the title
|
||||
* @param subtitle The TextView associated with the subtitle
|
||||
*/
|
||||
private class ItemViewHolder(val parent : View, var title : TextView, var subtitle : TextView? = null) : RecyclerView.ViewHolder(parent)
|
||||
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* The ViewHolder used by headers is used to hold the views associated with an headers
|
||||
*
|
||||
* @param parent The parent view that contains all the others
|
||||
* @param header The TextView associated with the header
|
||||
*/
|
||||
private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent)
|
||||
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
|
||||
/**
|
||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||
@ -87,14 +75,14 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
return when (ElementType.values()[viewType]) {
|
||||
ElementType.Item -> {
|
||||
if (compact) {
|
||||
ItemViewHolder(view, view.findViewById(R.id.text_title))
|
||||
ItemViewHolder(view)
|
||||
} else {
|
||||
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle))
|
||||
ItemViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
ElementType.Header -> {
|
||||
HeaderViewHolder(view, view.findViewById(R.id.text_title))
|
||||
HeaderViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,15 +94,15 @@ internal class LogAdapter internal constructor(val context : Context, val compac
|
||||
val item = getItem(position)
|
||||
|
||||
if (item is LogItem && holder is ItemViewHolder) {
|
||||
holder.title.text = item.message
|
||||
holder.subtitle?.text = item.level
|
||||
holder.text_title.text = item.message
|
||||
holder.text_subtitle?.text = item.level
|
||||
|
||||
holder.parent.setOnClickListener {
|
||||
holder.itemView.setOnClickListener {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
|
||||
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header.text = item.title
|
||||
holder.text_title.text = item.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class ControllerActivity : AppCompatActivity() {
|
||||
/**
|
||||
* The index of the controller this activity manages
|
||||
*/
|
||||
var id : Int = -1
|
||||
val id by lazy { intent.getIntExtra("index", 0) }
|
||||
|
||||
/**
|
||||
* The adapter used by [controller_list] to hold all the items
|
||||
@ -55,6 +55,8 @@ class ControllerActivity : AppCompatActivity() {
|
||||
if (controller.type == ControllerType.None)
|
||||
return
|
||||
|
||||
|
||||
|
||||
var wroteTitle = false
|
||||
|
||||
for (item in GeneralType.values()) {
|
||||
@ -128,8 +130,6 @@ class ControllerActivity : AppCompatActivity() {
|
||||
override fun onCreate(state : Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
id = intent.getIntExtra("index", 0)
|
||||
|
||||
if (id < 0 || id > 7)
|
||||
throw IllegalArgumentException()
|
||||
|
||||
@ -159,7 +159,7 @@ class ControllerActivity : AppCompatActivity() {
|
||||
is ControllerTypeItem -> {
|
||||
val controller = InputManager.controllers[id]!!
|
||||
|
||||
val types = ControllerType.values().filter { !it.firstController || id == 0 }
|
||||
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
|
@ -87,7 +87,7 @@ class OnScreenControllerView @JvmOverloads constructor(
|
||||
val outerToInner = joystick.outerToInner()
|
||||
val outerToInnerLength = outerToInner.length()
|
||||
val direction = outerToInner.normalize()
|
||||
val duration = (150f * outerToInnerLength / radius).roundToLong()
|
||||
val duration = (50f * outerToInnerLength / radius).roundToLong()
|
||||
joystickAnimators[joystick] = ValueAnimator.ofFloat(outerToInnerLength, 0f).apply {
|
||||
addUpdateListener { animation ->
|
||||
val value = animation.animatedValue as Float
|
||||
@ -106,6 +106,8 @@ class OnScreenControllerView @JvmOverloads constructor(
|
||||
|
||||
override fun onAnimationEnd(animation : Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
if (joystick.shortDoubleTapped)
|
||||
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Released)
|
||||
joystick.onFingerUp(event.x, event.y)
|
||||
invalidate()
|
||||
}
|
||||
@ -123,6 +125,8 @@ class OnScreenControllerView @JvmOverloads constructor(
|
||||
joystickAnimators[joystick] = null
|
||||
joystick.touchPointerId = pointerId
|
||||
joystick.onFingerDown(x, y)
|
||||
if (joystick.shortDoubleTapped)
|
||||
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Pressed)
|
||||
performClick()
|
||||
handled = true
|
||||
}
|
||||
@ -139,7 +143,7 @@ class OnScreenControllerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
handled.also { if (it) invalidate() }
|
||||
handled.also { if (it) invalidate() else super.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
private val editingTouchHandler = OnTouchListener { _, event ->
|
||||
@ -169,7 +173,7 @@ class OnScreenControllerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
false
|
||||
}.also { handled -> if (handled) invalidate() }
|
||||
}.also { handled -> if (handled) invalidate() else super.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -7,6 +7,7 @@ package emu.skyline.input.onscreen
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.PointF
|
||||
import android.os.SystemClock
|
||||
import androidx.core.graphics.minus
|
||||
import emu.skyline.R
|
||||
import emu.skyline.input.ButtonId
|
||||
@ -59,6 +60,11 @@ class JoystickButton(
|
||||
|
||||
private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
|
||||
|
||||
private var fingerDownTime = 0L
|
||||
private var fingerUpTime = 0L
|
||||
var shortDoubleTapped = false
|
||||
private set
|
||||
|
||||
override fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float) = Unit
|
||||
|
||||
override fun render(canvas : Canvas) {
|
||||
@ -74,12 +80,23 @@ class JoystickButton(
|
||||
relativeY = (y - heightDiff) / adjustedHeight
|
||||
innerButton.relativeX = relativeX
|
||||
innerButton.relativeY = relativeY
|
||||
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val firstTapDiff = fingerUpTime - fingerDownTime
|
||||
val secondTapDiff = currentTime - fingerUpTime
|
||||
if (firstTapDiff in 0..500 && secondTapDiff in 0..500) {
|
||||
shortDoubleTapped = true
|
||||
}
|
||||
fingerDownTime = currentTime
|
||||
}
|
||||
|
||||
override fun onFingerUp(x : Float, y : Float) {
|
||||
loadConfigValues()
|
||||
innerButton.relativeX = relativeX
|
||||
innerButton.relativeY = relativeY
|
||||
|
||||
fingerUpTime = SystemClock.elapsedRealtime()
|
||||
shortDoubleTapped = false
|
||||
}
|
||||
|
||||
fun onFingerMoved(x : Float, y : Float) : PointF {
|
||||
@ -91,6 +108,11 @@ class JoystickButton(
|
||||
finger = position.add(outerToInner.multiply(1f / distance * radius))
|
||||
}
|
||||
|
||||
if (distance > radius * 0.075f) {
|
||||
fingerDownTime = 0
|
||||
fingerUpTime = 0
|
||||
}
|
||||
|
||||
innerButton.relativeX = finger.x / width
|
||||
innerButton.relativeY = (finger.y - heightDiff) / adjustedHeight
|
||||
return finger.minus(position).multiply(1f / radius)
|
||||
@ -100,6 +122,7 @@ class JoystickButton(
|
||||
|
||||
override fun edit(x : Float, y : Float) {
|
||||
super.edit(x, y)
|
||||
|
||||
innerButton.relativeX = relativeX
|
||||
innerButton.relativeY = relativeY
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/app_item_grid"
|
||||
android:id="@+id/card_app_item_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/app_item_grid"
|
||||
android:id="@+id/card_app_item_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
Loading…
Reference in New Issue
Block a user