Merge remote-tracking branch 'origin/master' into 1.5
This commit is contained in:
@@ -36,6 +36,7 @@ namespace FlaxEditor.Content
|
||||
/// A <see cref="SceneAnimation"/> asset proxy object.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Content.BinaryAssetProxy" />
|
||||
[ContentContextMenu("New/Animation/Scene Animation")]
|
||||
public class SceneAnimationProxy : BinaryAssetProxy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -9,6 +9,7 @@ using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.Content.Settings;
|
||||
using FlaxEditor.Content.Thumbnails;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Modules;
|
||||
using FlaxEditor.Modules.SourceCodeEditing;
|
||||
using FlaxEditor.Options;
|
||||
@@ -46,6 +47,8 @@ namespace FlaxEditor
|
||||
private bool _isAfterInit, _areModulesInited, _areModulesAfterInitEnd, _isHeadlessMode;
|
||||
private string _projectToOpen;
|
||||
private float _lastAutoSaveTimer;
|
||||
private AutoSavePopup _autoSavePopup;
|
||||
private bool _autoSaveNow;
|
||||
private Guid _startupSceneCmdLine;
|
||||
|
||||
private const string ProjectDataLastScene = "LastScene";
|
||||
@@ -491,7 +494,28 @@ namespace FlaxEditor
|
||||
var timeToNextSave = options.AutoSaveFrequency * 60.0f - timeSinceLastSave;
|
||||
var countDownDuration = 4.0f;
|
||||
|
||||
if (timeToNextSave <= 0.0f)
|
||||
// Show auto save popup
|
||||
if (timeToNextSave <= options.AutoSaveReminderTime && timeToNextSave >= 0)
|
||||
{
|
||||
if (_autoSavePopup == null)
|
||||
{
|
||||
_autoSavePopup = AutoSavePopup.Show(Instance.Windows.MainWindow.GUI, timeToNextSave);
|
||||
_autoSavePopup.SaveNowButton.Clicked += () => _autoSaveNow = true;
|
||||
_autoSavePopup.CancelSaveButton.Clicked += () =>
|
||||
{
|
||||
Log("Auto save canceled");
|
||||
_autoSavePopup.HidePopup();
|
||||
_lastAutoSaveTimer = Time.UnscaledGameTime; // Reset timer
|
||||
};
|
||||
}
|
||||
else if (!_autoSavePopup.Visible && !_autoSavePopup.UserClosed)
|
||||
_autoSavePopup.ShowPopup();
|
||||
|
||||
if (_autoSavePopup.Visible)
|
||||
_autoSavePopup.UpdateTime(timeToNextSave);
|
||||
}
|
||||
|
||||
if (timeToNextSave <= 0.0f || _autoSaveNow)
|
||||
{
|
||||
Log("Auto save");
|
||||
_lastAutoSaveTimer = Time.UnscaledGameTime;
|
||||
@@ -499,6 +523,11 @@ namespace FlaxEditor
|
||||
Scene.SaveScenes();
|
||||
if (options.AutoSaveContent)
|
||||
SaveContent();
|
||||
|
||||
// Hide auto save popup and reset user closed
|
||||
_autoSavePopup.HidePopup();
|
||||
_autoSavePopup.UserClosed = false;
|
||||
_autoSaveNow = false;
|
||||
}
|
||||
else if (timeToNextSave < countDownDuration)
|
||||
{
|
||||
|
||||
169
Source/Editor/GUI/Popups/AutoSavePopup.cs
Normal file
169
Source/Editor/GUI/Popups/AutoSavePopup.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.GUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Popup menu for reminding of an upcoming auto save.
|
||||
/// </summary>
|
||||
public class AutoSavePopup : ContainerControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value for if the user has manually closed this popup.
|
||||
/// </summary>
|
||||
public bool UserClosed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The save now button. Used to skip the last remaining auto save time.
|
||||
/// </summary>
|
||||
public Button SaveNowButton => _saveNowButton;
|
||||
|
||||
/// <summary>
|
||||
/// Button that should be used to cancel the auto save.
|
||||
/// </summary>
|
||||
public Button CancelSaveButton => _cancelSaveButton;
|
||||
|
||||
private int _timeRemaining;
|
||||
private Panel _backgroundPanel;
|
||||
private Label _timeLabel;
|
||||
private Button _saveNowButton;
|
||||
private Button _cancelSaveButton;
|
||||
private Color _defaultTextColor;
|
||||
private bool _isMoved = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the AutoSavePopup.
|
||||
/// </summary>
|
||||
/// <param name="initialTime">The initial time to set the time label to.</param>
|
||||
public AutoSavePopup(float initialTime)
|
||||
{
|
||||
UpdateTime(initialTime);
|
||||
|
||||
_backgroundPanel = new Panel(ScrollBars.None)
|
||||
{
|
||||
Parent = this,
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
BackgroundColor = Color.Transparent,
|
||||
};
|
||||
|
||||
_timeLabel = new Label(0, 0, 25, 10)
|
||||
{
|
||||
Parent = _backgroundPanel,
|
||||
Text = _timeRemaining.ToString(),
|
||||
HorizontalAlignment = TextAlignment.Near,
|
||||
VerticalAlignment = TextAlignment.Near,
|
||||
AutoWidth = true,
|
||||
Height = 14,
|
||||
AnchorPreset = AnchorPresets.MiddleLeft,
|
||||
};
|
||||
|
||||
_saveNowButton = new Button
|
||||
{
|
||||
Parent = _backgroundPanel,
|
||||
Height = 14,
|
||||
Width = 60,
|
||||
AnchorPreset = AnchorPresets.MiddleRight,
|
||||
BackgroundColor = Color.Transparent,
|
||||
BorderColor = Color.Transparent,
|
||||
BackgroundColorHighlighted = Color.Transparent,
|
||||
BackgroundColorSelected = Color.Transparent,
|
||||
BorderColorHighlighted = Color.Transparent,
|
||||
Text = "Save Now",
|
||||
TooltipText = "Saves now and restarts the auto save timer."
|
||||
};
|
||||
_saveNowButton.LocalX -= 85;
|
||||
|
||||
_cancelSaveButton = new Button
|
||||
{
|
||||
Parent = _backgroundPanel,
|
||||
Height = 14,
|
||||
Width = 70,
|
||||
AnchorPreset = AnchorPresets.MiddleRight,
|
||||
BackgroundColor = Color.Transparent,
|
||||
BorderColor = Color.Transparent,
|
||||
BackgroundColorHighlighted = Color.Transparent,
|
||||
BackgroundColorSelected = Color.Transparent,
|
||||
BorderColorHighlighted = Color.Transparent,
|
||||
Text = "Cancel Save",
|
||||
TooltipText = "Cancels this auto save."
|
||||
};
|
||||
_cancelSaveButton.LocalX -= 5;
|
||||
|
||||
_defaultTextColor = _saveNowButton.TextColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the time label
|
||||
/// </summary>
|
||||
/// <param name="time">The time to change the label to.</param>
|
||||
public void UpdateTime(float time)
|
||||
{
|
||||
_timeRemaining = Mathf.CeilToInt(time);
|
||||
if (_timeLabel != null)
|
||||
_timeLabel.Text = "Auto Save in: " + _timeRemaining;
|
||||
|
||||
// Move on text update if the progress bar is visible - removes this call from update
|
||||
if (Editor.Instance.UI.ProgressVisible && !_isMoved)
|
||||
{
|
||||
_isMoved = true;
|
||||
LocalX -= 250;
|
||||
}
|
||||
else if (!Editor.Instance.UI.ProgressVisible && _isMoved)
|
||||
{
|
||||
LocalX += 250;
|
||||
_isMoved = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (IsMouseOver)
|
||||
{
|
||||
_saveNowButton.TextColor = _saveNowButton.IsMouseOver ? Style.Current.BackgroundHighlighted : _defaultTextColor;
|
||||
_cancelSaveButton.TextColor = _cancelSaveButton.IsMouseOver ? Style.Current.BackgroundHighlighted : _defaultTextColor;
|
||||
}
|
||||
base.Update(deltaTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and shows the AutoSavePopup
|
||||
/// </summary>
|
||||
/// <param name="parentControl">The parent control.</param>
|
||||
/// <param name="initialTime">The time to start at.</param>
|
||||
/// <returns></returns>
|
||||
public static AutoSavePopup Show(ContainerControl parentControl, float initialTime)
|
||||
{
|
||||
var popup = new AutoSavePopup(initialTime)
|
||||
{
|
||||
Parent = parentControl,
|
||||
Height = Editor.Instance.UI.StatusBar.Height,
|
||||
Width = 250,
|
||||
AnchorPreset = AnchorPresets.BottomRight,
|
||||
};
|
||||
popup.ShowPopup();
|
||||
return popup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the popup by changing its visibility
|
||||
/// </summary>
|
||||
public void ShowPopup()
|
||||
{
|
||||
Visible = true;
|
||||
UserClosed = false;
|
||||
_saveNowButton.TextColor = _defaultTextColor;
|
||||
_cancelSaveButton.TextColor = _defaultTextColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the popup by changing its visibility
|
||||
/// </summary>
|
||||
public void HidePopup()
|
||||
{
|
||||
Visible = false;
|
||||
if (_containsFocus)
|
||||
Defocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,19 +196,26 @@ namespace FlaxEditor.Options
|
||||
[DefaultValue(5), Limit(1)]
|
||||
[EditorDisplay("Auto Save", "Auto Save Frequency"), EditorOrder(801), Tooltip("The interval between auto saves (in minutes)")]
|
||||
public int AutoSaveFrequency { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the time before the auto save that the popup shows (in seconds).
|
||||
/// </summary>
|
||||
[DefaultValue(10), Limit(-1)]
|
||||
[EditorDisplay("Auto Save", "Auto Save Reminder Time"), EditorOrder(802), Tooltip("The time before the auto save that the reminder popup shows (in seconds). Set to -1 to not show the reminder popup.")]
|
||||
public int AutoSaveReminderTime { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether enable auto saves for scenes.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
[EditorDisplay("Auto Save", "Auto Save Scenes"), EditorOrder(802), Tooltip("Enables or disables auto saving opened scenes")]
|
||||
[EditorDisplay("Auto Save", "Auto Save Scenes"), EditorOrder(803), Tooltip("Enables or disables auto saving opened scenes")]
|
||||
public bool AutoSaveScenes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether enable auto saves for content.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
[EditorDisplay("Auto Save", "Auto Save Content"), EditorOrder(803), Tooltip("Enables or disables auto saving content")]
|
||||
[EditorDisplay("Auto Save", "Auto Save Content"), EditorOrder(804), Tooltip("Enables or disables auto saving content")]
|
||||
public bool AutoSaveContent { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -405,6 +405,31 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
NodeElementArchetype.Factory.Output(0, "Color", typeof(Float3), 3)
|
||||
}
|
||||
},
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 17,
|
||||
Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch),
|
||||
Title = "Procedural Sample Texture",
|
||||
Description = "Samples a texture to create a more natural look with less obvious tiling.",
|
||||
Flags = NodeFlags.MaterialGraph,
|
||||
Size = new Float2(240, 110),
|
||||
ConnectionsHints = ConnectionsHint.Vector,
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
2,
|
||||
-1.0f,
|
||||
0,
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
NodeElementArchetype.Factory.Input(0, "Texture", true, typeof(FlaxEngine.Object), 0),
|
||||
NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1),
|
||||
NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3),
|
||||
NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4),
|
||||
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"),
|
||||
NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType))
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +397,13 @@ namespace FlaxEditor.Surface.ContextMenu
|
||||
return;
|
||||
|
||||
Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged");
|
||||
|
||||
if (string.IsNullOrEmpty(_searchBox.Text))
|
||||
{
|
||||
ResetView();
|
||||
Profiler.EndEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update groups
|
||||
LockChildrenRecursive();
|
||||
|
||||
@@ -906,12 +906,21 @@ namespace FlaxEditor.Utilities
|
||||
if (name.StartsWith("g_") || name.StartsWith("m_"))
|
||||
startIndex = 2;
|
||||
|
||||
if (name.StartsWith("_"))
|
||||
startIndex = 1;
|
||||
|
||||
// Filter text
|
||||
var lastChar = '\0';
|
||||
for (int i = startIndex; i < length; i++)
|
||||
{
|
||||
var c = name[i];
|
||||
|
||||
if (i == startIndex)
|
||||
{
|
||||
sb.Append(char.ToUpper(c));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Space before word starting with uppercase letter
|
||||
if (char.IsUpper(c) && i > 0)
|
||||
{
|
||||
|
||||
@@ -459,6 +459,8 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
}
|
||||
// Sample Texture
|
||||
case 9:
|
||||
// Procedural Texture Sample
|
||||
case 17:
|
||||
{
|
||||
enum CommonSamplerType
|
||||
{
|
||||
@@ -479,7 +481,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
// Get input boxes
|
||||
auto textureBox = node->GetBox(0);
|
||||
auto uvsBox = node->GetBox(1);
|
||||
auto levelBox = node->GetBox(2);
|
||||
auto levelBox = node->TryGetBox(2);
|
||||
auto offsetBox = node->GetBox(3);
|
||||
if (!textureBox->HasConnection())
|
||||
{
|
||||
@@ -519,7 +521,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
|
||||
// Get other inputs
|
||||
const auto level = tryGetValue(levelBox, node->Values[1]);
|
||||
const bool useLevel = levelBox->HasConnection() || (int32)node->Values[1] != -1;
|
||||
const bool useLevel = (levelBox && levelBox->HasConnection()) || (int32)node->Values[1] != -1;
|
||||
const bool useOffset = offsetBox->HasConnection();
|
||||
const auto offset = useOffset ? eatBox(offsetBox->GetParent<Node>(), offsetBox->FirstConnection()) : Value::Zero;
|
||||
const Char* samplerName;
|
||||
@@ -539,32 +541,78 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick a property format string
|
||||
const Char* format;
|
||||
if (useLevel || !canUseSample)
|
||||
// Create texture sampling code
|
||||
if (node->TypeID == 9)
|
||||
{
|
||||
if (useOffset)
|
||||
format = TEXT("{0}.SampleLevel({1}, {2}, {3}, {4})");
|
||||
// Sample Texture
|
||||
const Char* format;
|
||||
if (useLevel || !canUseSample)
|
||||
{
|
||||
if (useOffset)
|
||||
format = TEXT("{0}.SampleLevel({1}, {2}, {3}, {4})");
|
||||
else
|
||||
format = TEXT("{0}.SampleLevel({1}, {2}, {3})");
|
||||
}
|
||||
else
|
||||
format = TEXT("{0}.SampleLevel({1}, {2}, {3})");
|
||||
{
|
||||
if (useOffset)
|
||||
format = TEXT("{0}.Sample({1}, {2}, {4})");
|
||||
else
|
||||
format = TEXT("{0}.Sample({1}, {2})");
|
||||
}
|
||||
const String sampledValue = String::Format(format, texture.Value, samplerName, uvs.Value, level.Value, offset.Value);
|
||||
textureBox->Cache = writeLocal(VariantType::Float4, sampledValue, node);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (useOffset)
|
||||
format = TEXT("{0}.Sample({1}, {2}, {4})");
|
||||
else
|
||||
format = TEXT("{0}.Sample({1}, {2})");
|
||||
}
|
||||
// Procedural Texture Sample
|
||||
textureBox->Cache = writeLocal(Value::InitForZero(ValueType::Float4), node);
|
||||
createGradients(node);
|
||||
auto proceduralSample = String::Format(TEXT(
|
||||
" {{\n"
|
||||
" float3 weights;\n"
|
||||
" float2 vertex1, vertex2, vertex3;\n"
|
||||
" float2 uv = {0} * 3.464; // 2 * sqrt (3);\n"
|
||||
" float2 uv1, uv2, uv3;\n"
|
||||
" const float2x2 gridToSkewedGrid = float2x2(1.0, 0.0, -0.57735027, 1.15470054);\n"
|
||||
" float2 skewedCoord = mul(gridToSkewedGrid, uv);\n"
|
||||
" int2 baseId = int2(floor(skewedCoord));\n"
|
||||
" float3 temp = float3(frac(skewedCoord), 0);\n"
|
||||
" temp.z = 1.0 - temp.x - temp.y;\n"
|
||||
" if (temp.z > 0.0)\n"
|
||||
" {{\n"
|
||||
" weights = float3(temp.z, temp.y, temp.x);\n"
|
||||
" vertex1 = baseId;\n"
|
||||
" vertex2 = baseId + int2(0, 1);\n"
|
||||
" vertex3 = baseId + int2(1, 0);\n"
|
||||
" }}\n"
|
||||
" else\n"
|
||||
" {{\n"
|
||||
" weights = float3(-temp.z, 1.0 - temp.y, 1.0 - temp.x);\n"
|
||||
" vertex1 = baseId + int2(1, 1);\n"
|
||||
" vertex2 = baseId + int2(1, 0);\n"
|
||||
" vertex3 = baseId + int2(0, 1);\n"
|
||||
" }}\n"
|
||||
" uv1 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex1)) * 43758.5453);\n"
|
||||
" uv2 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex2)) * 43758.5453);\n"
|
||||
" uv3 = {0} + frac(sin(mul(float2x2(127.1, 311.7, 269.5, 183.3), vertex3)) * 43758.5453);\n"
|
||||
" float4 tex1 = {1}.SampleGrad({4}, uv1, {2}, {3}, {6}) * weights.x;\n"
|
||||
" float4 tex2 = {1}.SampleGrad({4}, uv2, {2}, {3}, {6}) * weights.y;\n"
|
||||
" float4 tex3 = {1}.SampleGrad({4}, uv3, {2}, {3}, {6}) * weights.z;\n"
|
||||
" {5} = tex1 + tex2 + tex3;\n"
|
||||
" }}\n"
|
||||
),
|
||||
uvs.Value, // {0}
|
||||
texture.Value, // {1}
|
||||
_ddx.Value, // {2}
|
||||
_ddy.Value, // {3}
|
||||
samplerName, // {4}
|
||||
textureBox->Cache.Value, // {5}
|
||||
offset.Value // {6}
|
||||
);
|
||||
|
||||
// Sample texture
|
||||
const String sampledValue = String::Format(format,
|
||||
texture.Value, // {0}
|
||||
samplerName, // {1}
|
||||
uvs.Value, // {2}
|
||||
level.Value, // {3}
|
||||
offset.Value // {4}
|
||||
);
|
||||
textureBox->Cache = writeLocal(VariantType::Float4, sampledValue, node);
|
||||
_writer.Write(*proceduralSample);
|
||||
}
|
||||
|
||||
// Decode normal map vector
|
||||
if (isNormalMap)
|
||||
@@ -627,7 +675,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
auto textureBox = node->GetBox(0);
|
||||
auto scaleBox = node->GetBox(1);
|
||||
auto blendBox = node->GetBox(2);
|
||||
|
||||
|
||||
if (!textureBox->HasConnection())
|
||||
{
|
||||
// No texture to sample
|
||||
@@ -660,10 +708,10 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
|
||||
" {3} += {0}.Sample(SamplerLinearWrap, worldPos.xy) * normal.z;\n"
|
||||
" }}\n"
|
||||
),
|
||||
texture.Value, // {0}
|
||||
scale.Value, // {1}
|
||||
blend.Value, // {2}
|
||||
result.Value // {3}
|
||||
texture.Value, // {0}
|
||||
scale.Value, // {1}
|
||||
blend.Value, // {2}
|
||||
result.Value // {3}
|
||||
);
|
||||
|
||||
_writer.Write(*triplanarTexture);
|
||||
|
||||
@@ -11,10 +11,10 @@ namespace FlaxEditor.Tests
|
||||
[Test]
|
||||
public void TestFormatting()
|
||||
{
|
||||
Assert.AreEqual("property", Utils.GetPropertyNameUI("property"));
|
||||
Assert.AreEqual("property", Utils.GetPropertyNameUI("_property"));
|
||||
Assert.AreEqual("property", Utils.GetPropertyNameUI("m_property"));
|
||||
Assert.AreEqual("property", Utils.GetPropertyNameUI("g_property"));
|
||||
Assert.AreEqual("Property", Utils.GetPropertyNameUI("property"));
|
||||
Assert.AreEqual("Property", Utils.GetPropertyNameUI("_property"));
|
||||
Assert.AreEqual("Property", Utils.GetPropertyNameUI("m_property"));
|
||||
Assert.AreEqual("Property", Utils.GetPropertyNameUI("g_property"));
|
||||
Assert.AreEqual("Property", Utils.GetPropertyNameUI("Property"));
|
||||
Assert.AreEqual("Property 1", Utils.GetPropertyNameUI("Property1"));
|
||||
Assert.AreEqual("Property Name", Utils.GetPropertyNameUI("PropertyName"));
|
||||
|
||||
Reference in New Issue
Block a user