气泡组件和配置项
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 626c63a530654b14f9c7a3be634dd0b5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DialogBox : DialogBoxBase
|
||||
{
|
||||
[SerializeField] protected GameObject choiceButton;
|
||||
|
||||
protected override void LoadChoiceButton()
|
||||
{
|
||||
choiceButtonPrefab = choiceButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 51f2233614aba944f9b2dab3d5af433b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using AibisDream.Config;
|
||||
using AibisDream.Utility;
|
||||
using Febucci.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public abstract class DialogBoxBase : MonoBehaviour, IDialogView
|
||||
{
|
||||
#region box组件名称
|
||||
|
||||
private const string ActorName = "Actor Name";
|
||||
private const string DialogText = "Dialog Text";
|
||||
private const string NextArray = "Next Array";
|
||||
private const string ChoiceBox = "Choice Box";
|
||||
|
||||
#endregion
|
||||
|
||||
#region box组件
|
||||
|
||||
protected TypewriterByCharacter dialogText;
|
||||
protected TextMeshProUGUI actorName;
|
||||
protected GameObject nextArray;
|
||||
protected GameObject choiceBox;
|
||||
|
||||
#endregion
|
||||
|
||||
private bool _skipLineDelay;
|
||||
[Header("跳过阻塞延迟时间/ms")]
|
||||
[SerializeField] private int delayTime = 300;
|
||||
|
||||
protected GameObject choiceButtonPrefab;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
LoadChoiceButton();
|
||||
InitComponents();
|
||||
AddSomeListener();
|
||||
}
|
||||
|
||||
private void AddSomeListener()
|
||||
{
|
||||
// 对话显示后有一段延时不可跳过
|
||||
dialogText.onTypewriterStart.AddListener(() =>
|
||||
{
|
||||
_skipLineDelay = true;
|
||||
CommonUtil.Delay(delayTime, () => { _skipLineDelay = false; });
|
||||
});
|
||||
}
|
||||
|
||||
#region 初始化
|
||||
|
||||
protected virtual void LoadChoiceButton()
|
||||
{
|
||||
Debug.Log("无选项对话框");
|
||||
}
|
||||
|
||||
private void InitComponents()
|
||||
{
|
||||
actorName = transform.Find(ActorName)?.gameObject.GetComponent<TextMeshProUGUI>();
|
||||
dialogText = transform.Find(DialogText)?.gameObject.GetComponent<TypewriterByCharacter>();
|
||||
nextArray = transform.Find(NextArray)?.gameObject;
|
||||
choiceBox = transform.Find(ChoiceBox)?.gameObject;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏对话盒
|
||||
/// </summary>
|
||||
public void HideDialog()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示对话
|
||||
/// </summary>
|
||||
/// <param name="dialogLine">对话内容</param>
|
||||
/// <param name="character">角色内容</param>
|
||||
/// <param name="nextStep">下一步</param>
|
||||
/// <param name="isAutoSkip">是否自动跳过,默认为false</param>
|
||||
public virtual void ShowLine(string dialogLine, CharacterVo character, Action nextStep, bool isAutoSkip = false)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
// 没有dialogText说明不能显示对话,直接跳过
|
||||
if (!dialogText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 把原有对话清掉
|
||||
ClearBox();
|
||||
dialogText.gameObject.SetActive(true);
|
||||
choiceBox?.SetActive(false);
|
||||
|
||||
// 显示内容
|
||||
ShowActorName(character);
|
||||
dialogText.ShowText(dialogLine);
|
||||
|
||||
// TODO 如果有头像的对话框要加入头像处理
|
||||
|
||||
// 自动跳过的回调很难独立Remove,只能全部清空再注册,下策
|
||||
dialogText.onTextShowed.RemoveAllListeners();
|
||||
dialogText.onTextShowed.AddListener(() => nextArray.gameObject.SetActive(true));
|
||||
|
||||
// 执行自动跳过结局
|
||||
if (isAutoSkip)
|
||||
{
|
||||
dialogText.onTextShowed.AddListener(() => CommonUtil.Delay(200, nextStep));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 展示选项
|
||||
/// </summary>
|
||||
/// <param name="dialogueOptions">对话选项</param>
|
||||
/// <param name="onOptionSelected">选项选择</param>
|
||||
public virtual void ShowOptions(DialogueOption[] dialogueOptions, Action<int> onOptionSelected)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
// 没有选择框直接不显示选项
|
||||
if (!choiceBox)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 把原有对话清掉
|
||||
ClearBox();
|
||||
choiceBox.SetActive(true);
|
||||
dialogText.gameObject.SetActive(false);
|
||||
|
||||
// 销毁原来的按钮
|
||||
foreach (Transform child in choiceBox.transform)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 生成新按钮
|
||||
foreach (DialogueOption option in dialogueOptions)
|
||||
{
|
||||
if (!option.IsAvailable) continue;
|
||||
|
||||
var choiceInstance = Instantiate(choiceButtonPrefab, choiceBox.transform);
|
||||
choiceInstance.GetComponentInChildren<TextMeshProUGUI>().text =
|
||||
option.Line.TextWithoutCharacterName.Text;
|
||||
choiceInstance.GetComponent<Button>().onClick
|
||||
.AddListener(() => onOptionSelected.Invoke(option.DialogueOptionID));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试跳过对话
|
||||
/// true说明成功快进了对话显示
|
||||
/// false说明对话已经显示完成了,可以跳过
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual bool TrySkipLine()
|
||||
{
|
||||
if (_skipLineDelay)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dialogText.isShowingText)
|
||||
{
|
||||
dialogText.SkipTypewriter();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextArray.gameObject.SetActive(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearBox()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
dialogText?.ShowText("");
|
||||
nextArray?.SetActive(false);
|
||||
if (!actorName) actorName.text = "";
|
||||
}
|
||||
|
||||
private void ShowActorName(CharacterVo character)
|
||||
{
|
||||
if (!actorName) return;
|
||||
|
||||
if (string.IsNullOrEmpty(character.nameColor))
|
||||
{
|
||||
actorName.text = character.cn;
|
||||
}
|
||||
else
|
||||
{
|
||||
var showStr = $"<color={character.nameColor}>{character.cn}</color>";
|
||||
actorName.text = showStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a4b8d325dce1b64980f8fd2ff77c571
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DialogBubble : DialogBoxBase
|
||||
{
|
||||
// 气泡功能比较简单似乎不需要加别的
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d43fa24173f81ea4ea102be91896826e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AibisDream.Config;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DialogBubbleViewer : MonoBehaviour, IDialogView
|
||||
{
|
||||
private const string PlayerBubble = "Player Bubble";
|
||||
private const string ActorBubble = "Actor Bubble";
|
||||
private const string NormalBox = "Normal Box";
|
||||
|
||||
private IDialogView _normalBox;
|
||||
private IDialogView _playerBubble;
|
||||
private IDialogView _actorBubble;
|
||||
// 应该会有自定义位置的泡泡,但目前还没想好怎么做
|
||||
private Dictionary<string, IDialogView> _customBubblePool = new();
|
||||
|
||||
private List<IDialogView> _dialogViews;
|
||||
|
||||
private IDialogView _currentView;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Init();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
_normalBox = transform.Find(NormalBox).GetComponent<IDialogView>();
|
||||
_playerBubble = transform.Find(PlayerBubble).GetComponent<IDialogView>();
|
||||
_actorBubble = transform.Find(ActorBubble).GetComponent<IDialogView>();
|
||||
|
||||
_dialogViews = new List<IDialogView> { _normalBox, _playerBubble, _actorBubble };
|
||||
_dialogViews.ForEach(item => item.HideDialog());
|
||||
}
|
||||
|
||||
public void ShowLine(string dialogLine, CharacterVo character, Action nextStep, bool isAutoSkip = false)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
// 根据角色信息气泡
|
||||
_currentView = SelectBubbleByCharacter(character);
|
||||
// 处理其他泡泡
|
||||
ProcessOtherBox();
|
||||
// 显示对话
|
||||
_currentView.ShowLine(dialogLine, character, nextStep, isAutoSkip);
|
||||
}
|
||||
|
||||
public void ShowOptions(DialogueOption[] dialogueOptions, Action<int> onOptionSelected)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
_currentView = _normalBox;
|
||||
// 目前只有正常Box可以展示选项
|
||||
// 处理其他泡泡
|
||||
ProcessOtherBox();
|
||||
|
||||
// 展示选项
|
||||
_normalBox.ShowOptions(dialogueOptions, onOptionSelected);
|
||||
}
|
||||
|
||||
public void HideDialog()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
// 隐藏整个对话组件
|
||||
_dialogViews?.ForEach(item => item.HideDialog());
|
||||
}
|
||||
|
||||
public bool TrySkipLine()
|
||||
{
|
||||
return _currentView == null || _currentView.TrySkipLine();
|
||||
}
|
||||
|
||||
private IDialogView SelectBubbleByCharacter(CharacterVo characterVo)
|
||||
{
|
||||
return characterVo.role switch
|
||||
{
|
||||
ActorRole.Player => _playerBubble,
|
||||
ActorRole.MainActor => _actorBubble,
|
||||
ActorRole.Aside => _normalBox,
|
||||
_ => _normalBox
|
||||
};
|
||||
}
|
||||
|
||||
private void ProcessOtherBox()
|
||||
{
|
||||
if (_currentView == _actorBubble || _currentView == _playerBubble)
|
||||
{
|
||||
_normalBox.HideDialog();
|
||||
}
|
||||
|
||||
if (_currentView == _normalBox)
|
||||
{
|
||||
_actorBubble.HideDialog();
|
||||
_playerBubble.HideDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98b5d94e03211a94bba6de692a1d405a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DialogCanvasManager : Singleton<DialogCanvasManager>
|
||||
{
|
||||
private const string BubbleView = "Dialog Bubble View";
|
||||
private const string OldBoxView = "Dialog Box";
|
||||
|
||||
private const string QuickTalk = "Quick Talk";
|
||||
|
||||
private IDialogView _bubbleView;
|
||||
private IDialogView _oldBoxView;
|
||||
|
||||
private IDialogView _curView;
|
||||
|
||||
private GameObject _quickTalkButton;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
InitDialogViews();
|
||||
InitQuickTalkButton();
|
||||
// 默认为旧Box
|
||||
_curView = _bubbleView;
|
||||
}
|
||||
|
||||
#region 对话结束后隐藏
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
DialogController.OnDialogueComplete += HideDialog;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DialogController.OnDialogueComplete -= HideDialog;
|
||||
}
|
||||
|
||||
private void HideDialog()
|
||||
{
|
||||
_curView.HideDialog();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Q) && Input.GetKey(KeyCode.P) )
|
||||
{
|
||||
_quickTalkButton.SetActive(!_quickTalkButton.activeSelf);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitDialogViews()
|
||||
{
|
||||
_bubbleView = transform.Find(BubbleView).GetComponent<IDialogView>();
|
||||
_oldBoxView = transform.Find(OldBoxView).GetComponent<IDialogView>();
|
||||
|
||||
_bubbleView.HideDialog();
|
||||
_oldBoxView.HideDialog();
|
||||
}
|
||||
|
||||
private void InitQuickTalkButton()
|
||||
{
|
||||
_quickTalkButton = transform.Find(QuickTalk).gameObject;
|
||||
_quickTalkButton.GetComponent<Button>().onClick.AddListener(SwitchQuickTalkMode);
|
||||
}
|
||||
|
||||
private void SwitchQuickTalkMode()
|
||||
{
|
||||
DialogController.Instance.quickRunMode = !DialogController.Instance.quickRunMode;
|
||||
_quickTalkButton.GetComponent<Image>().color =
|
||||
DialogController.Instance.quickRunMode ? Color.gray : Color.white;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换对话UI
|
||||
/// </summary>
|
||||
/// <param name="viewType">UI类型</param>
|
||||
/// <returns></returns>
|
||||
public IDialogView SwitchDialogView(DialogViewType viewType)
|
||||
{
|
||||
var newDlg = viewType switch
|
||||
{
|
||||
DialogViewType.Bubble => _bubbleView,
|
||||
DialogViewType.OldBox => _oldBoxView,
|
||||
_ => _oldBoxView
|
||||
};
|
||||
|
||||
if (_curView != newDlg)
|
||||
{
|
||||
_curView.HideDialog();
|
||||
_curView = newDlg;
|
||||
}
|
||||
|
||||
return _curView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前正在使用的Dialog View
|
||||
/// </summary>
|
||||
/// <returns>当前正在使用的Dialog View</returns>
|
||||
/// <exception cref="Exception">未找到Canvas</exception>
|
||||
public static IDialogView GetCurView()
|
||||
{
|
||||
return Instance._curView;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DialogViewType
|
||||
{
|
||||
Bubble, OldBox
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c14c52217dac70244b02fe48d9462bae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AibisDream
|
||||
{
|
||||
public class DialogOldBox : DialogBoxBase
|
||||
{
|
||||
private void OnEnable()
|
||||
{
|
||||
DialogController.OnDialogueComplete += HideDialog;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DialogController.OnDialogueComplete -= HideDialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f063fd2d0404e04a809d7614c5b7532
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+13
-20
@@ -1,5 +1,6 @@
|
||||
using Febucci.UI;
|
||||
using System;
|
||||
using AibisDream.Utility;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -11,12 +12,12 @@ namespace AibisDream
|
||||
{
|
||||
#region 组件名称
|
||||
|
||||
private const string DIALOG_BOX = "Dialog Box";
|
||||
private const string ACTOR_NAME = "Actor Name";
|
||||
private const string DIALOG_TEXT = "Dialog Text";
|
||||
private const string NEXT_BUTTON = "Next Button";
|
||||
private const string CHOICE_BUTTON = "Choice Button";
|
||||
private const string QUICK_TALK = "Quick Talk";
|
||||
private const string DialogBox = "Dialog Box";
|
||||
private const string ActorName = "Actor Name";
|
||||
private const string DialogText = "Dialog Text";
|
||||
private const string NextButton = "Next Button";
|
||||
private const string ChoiceBox = "Choice Box";
|
||||
private const string QuickTalk = "Quick Talk";
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -40,7 +41,7 @@ namespace AibisDream
|
||||
// 获取预制体
|
||||
choiceButtonPrefab = Resources.Load<GameObject>("Prefabs/UI/Choice Button");
|
||||
// 获取子物体
|
||||
dialogBox = transform.Find(DIALOG_BOX).gameObject;
|
||||
dialogBox = transform.Find(DialogBox).gameObject;
|
||||
GetDialogComponentInBox(dialogBox);
|
||||
}
|
||||
|
||||
@@ -54,14 +55,6 @@ namespace AibisDream
|
||||
DialogController.OnDialogueComplete -= HideDialog;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Q) && Input.GetKey(KeyCode.P) )
|
||||
{
|
||||
quickTalkButton.SetActive(!quickTalkButton.activeSelf);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示文字
|
||||
/// </summary>
|
||||
@@ -176,11 +169,11 @@ namespace AibisDream
|
||||
|
||||
private void GetDialogComponentInBox(GameObject box)
|
||||
{
|
||||
actorName = box.transform.Find(ACTOR_NAME).gameObject.GetComponent<TextMeshProUGUI>();
|
||||
dialogText = box.transform.Find(DIALOG_TEXT).gameObject.GetComponent<TypewriterByCharacter>();
|
||||
nextButton = box.transform.Find(NEXT_BUTTON).GetComponent<Button>();
|
||||
choiceBox = box.transform.Find(CHOICE_BUTTON).gameObject;
|
||||
quickTalkButton = box.transform.Find(QUICK_TALK).gameObject;
|
||||
actorName = box.transform.Find(ActorName).gameObject.GetComponent<TextMeshProUGUI>();
|
||||
dialogText = box.transform.Find(DialogText).gameObject.GetComponent<TypewriterByCharacter>();
|
||||
nextButton = box.transform.Find(NextButton).GetComponent<Button>();
|
||||
choiceBox = box.transform.Find(ChoiceBox).gameObject;
|
||||
quickTalkButton = box.transform.Find(QuickTalk).gameObject;
|
||||
|
||||
quickTalkButton.GetComponent<Button>().onClick.AddListener(SwitchQuickTalkMode);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using AibisDream.Config;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
public interface IDialogView
|
||||
{
|
||||
public void ShowLine(string dialogLine, CharacterVo character, Action nextStep, bool isAutoSkip = false);
|
||||
|
||||
public void ShowOptions(DialogueOption[] dialogueOptions, Action<int> onOptionSelected);
|
||||
|
||||
public void HideDialog();
|
||||
|
||||
public bool TrySkipLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9f2abb67889d3247a1314a7788f8554
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,4 +12,3 @@ namespace AibisDream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user