Improve paper stamping experience (#17135)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Fax;
|
||||
using JetBrains.Annotations;
|
||||
@@ -15,7 +15,8 @@ public sealed class AdminFaxEui : BaseEui
|
||||
_window = new AdminFaxWindow();
|
||||
_window.OnClose += () => SendMessage(new AdminFaxEuiMsg.Close());
|
||||
_window.OnFollowFax += uid => SendMessage(new AdminFaxEuiMsg.Follow(uid));
|
||||
_window.OnMessageSend += args => SendMessage(new AdminFaxEuiMsg.Send(args.uid, args.title, args.from, args.message, args.stamp));
|
||||
_window.OnMessageSend += args => SendMessage(new AdminFaxEuiMsg.Send(args.uid, args.title,
|
||||
args.stampedBy, args.message, args.stampSprite, args.stampColor));
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc admin-fax-title}"
|
||||
MinWidth="400">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
@@ -21,6 +21,8 @@
|
||||
<Control MinWidth="5" />
|
||||
<OptionButton Name="StampSelector" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<Label Text="{Loc admin-fax-stamp-color}" />
|
||||
<ColorSelectorSliders Margin="12 0 0 0" Name="StampColorSelector" Color="#BB3232"/>
|
||||
<Control MinHeight="10" />
|
||||
<Button Name="SendButton" Text="{Loc admin-fax-send}"></Button>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Fax;
|
||||
using Content.Shared.Fax;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -13,7 +13,7 @@ public sealed partial class AdminFaxWindow : DefaultWindow
|
||||
{
|
||||
private const string StampsRsiPath = "/Textures/Objects/Misc/bureaucracy.rsi";
|
||||
|
||||
public Action<(EntityUid uid, string title, string from, string message, string stamp)>? OnMessageSend;
|
||||
public Action<(EntityUid uid, string title, string stampedBy, string message, string stampSprite, Color stampColor)>? OnMessageSend;
|
||||
public Action<EntityUid>? OnFollowFax;
|
||||
|
||||
public AdminFaxWindow()
|
||||
@@ -28,6 +28,9 @@ public sealed partial class AdminFaxWindow : DefaultWindow
|
||||
FollowButton.OnPressed += FollowFax;
|
||||
SendButton.OnPressed += SendMessage;
|
||||
|
||||
// Don't use this, but ColorSelectorSliders requires it:
|
||||
StampColorSelector.OnColorChanged += (Color) => {};
|
||||
|
||||
var loc = IoCManager.Resolve<ILocalizationManager>();
|
||||
MessageEdit.Placeholder = new Rope.Leaf(loc.GetString("admin-fax-message-placeholder")); // TextEdit work only with Nodes
|
||||
}
|
||||
@@ -90,6 +93,7 @@ public sealed partial class AdminFaxWindow : DefaultWindow
|
||||
return;
|
||||
|
||||
var from = FromEdit.Text;
|
||||
OnMessageSend?.Invoke((faxUid.Value, title, from, message, stamp));
|
||||
var stampColor = StampColorSelector.Color;
|
||||
OnMessageSend?.Invoke((faxUid.Value, title, from, message, stamp, stampColor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,67 +5,66 @@ using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Shared.Paper.SharedPaperComponent;
|
||||
|
||||
namespace Content.Client.Paper.UI
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PaperBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class PaperBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private PaperWindow? _window;
|
||||
|
||||
public PaperBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private PaperWindow? _window;
|
||||
}
|
||||
|
||||
public PaperBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new PaperWindow();
|
||||
_window.OnClose += Close;
|
||||
_window.Input.OnKeyBindDown += args => // Solution while TextEdit don't have events
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new PaperWindow();
|
||||
_window.OnClose += Close;
|
||||
_window.Input.OnKeyBindDown += args => // Solution while TextEdit don't have events
|
||||
if (args.Function == EngineKeyFunctions.TextSubmit)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.TextSubmit)
|
||||
{
|
||||
var text = Rope.Collapse(_window.Input.TextRope);
|
||||
Input_OnTextEntered(text);
|
||||
args.Handle();
|
||||
}
|
||||
};
|
||||
|
||||
if (EntMan.TryGetComponent<PaperVisualsComponent>(Owner, out var visuals))
|
||||
{
|
||||
_window.InitVisuals(visuals);
|
||||
var text = Rope.Collapse(_window.Input.TextRope);
|
||||
Input_OnTextEntered(text);
|
||||
args.Handle();
|
||||
}
|
||||
};
|
||||
|
||||
_window.OpenCentered();
|
||||
if (EntMan.TryGetComponent<PaperVisualsComponent>(Owner, out var visuals))
|
||||
{
|
||||
_window.InitVisuals(Owner, visuals);
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
_window?.Populate((PaperBoundUserInterfaceState) state);
|
||||
}
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void Input_OnTextEntered(string text)
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
_window?.Populate((PaperBoundUserInterfaceState) state);
|
||||
}
|
||||
|
||||
private void Input_OnTextEntered(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
SendMessage(new PaperInputTextMessage(text));
|
||||
|
||||
if (_window != null)
|
||||
{
|
||||
SendMessage(new PaperInputTextMessage(text));
|
||||
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Input.TextRope = Rope.Leaf.Empty;
|
||||
_window.Input.CursorPosition = new TextEdit.CursorPos(0, TextEdit.LineBreakBias.Top);
|
||||
}
|
||||
_window.Input.TextRope = Rope.Leaf.Empty;
|
||||
_window.Input.CursorPosition = new TextEdit.CursorPos(0, TextEdit.LineBreakBias.Top);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
_window?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
_window?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
<TextEdit Name="Input" StyleClasses="PaperLineEdit" Access="Public" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="StampDisplay" Orientation="Vertical" VerticalAlignment="Bottom" Margin="6"/>
|
||||
<paper:StampCollection Name="StampDisplay" VerticalAlignment="Bottom" Margin="6"/>
|
||||
|
||||
</PanelContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
@@ -44,8 +44,11 @@ namespace Content.Client.Paper.UI
|
||||
/// Initialize this UI according to <code>visuals</code> Initializes
|
||||
/// textures, recalculates sizes, and applies some layout rules.
|
||||
/// </summary>
|
||||
public void InitVisuals(PaperVisualsComponent visuals)
|
||||
public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals)
|
||||
{
|
||||
// Randomize the placement of any stamps based on the entity UID
|
||||
// so that there's some variety in different papers.
|
||||
StampDisplay.PlacementSeed = (int)entity;
|
||||
var resCache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
// Initialize the background:
|
||||
@@ -206,9 +209,10 @@ namespace Content.Client.Paper.UI
|
||||
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
|
||||
|
||||
StampDisplay.RemoveAllChildren();
|
||||
StampDisplay.RemoveStamps();
|
||||
foreach(var stamper in state.StampedBy)
|
||||
{
|
||||
StampDisplay.AddChild(new StampWidget{ Stamper = stamper });
|
||||
StampDisplay.AddStamp(new StampWidget{ StampInfo = stamper });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +224,7 @@ namespace Content.Client.Paper.UI
|
||||
/// </summary>
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
var mode = DragMode.Move;
|
||||
var mode = DragMode.None;
|
||||
|
||||
// Be quite generous with resize margins:
|
||||
if (relativeMousePos.Y < DRAG_MARGIN_SIZE)
|
||||
@@ -241,6 +245,10 @@ namespace Content.Client.Paper.UI
|
||||
mode |= DragMode.Right;
|
||||
}
|
||||
|
||||
if((mode & _allowedResizeModes) == DragMode.None)
|
||||
{
|
||||
return DragMode.Move;
|
||||
}
|
||||
return mode & _allowedResizeModes;
|
||||
}
|
||||
}
|
||||
|
||||
5
Content.Client/Paper/UI/StampCollection.xaml
Normal file
5
Content.Client/Paper/UI/StampCollection.xaml
Normal file
@@ -0,0 +1,5 @@
|
||||
<paper:StampCollection xmlns="https://spacestation14.io"
|
||||
xmlns:paper="clr-namespace:Content.Client.Paper.UI"
|
||||
MinSize="150 150">
|
||||
|
||||
</paper:StampCollection>
|
||||
98
Content.Client/Paper/UI/StampCollection.xaml.cs
Normal file
98
Content.Client/Paper/UI/StampCollection.xaml.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StampCollection : Container
|
||||
{
|
||||
private List<StampWidget> _stamps = new();
|
||||
|
||||
/// Seed for random number generator to place stamps deterministically
|
||||
public int PlacementSeed;
|
||||
|
||||
public StampCollection()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove any stamps from the page
|
||||
/// </summary>
|
||||
public void RemoveStamps()
|
||||
{
|
||||
_stamps.Clear();
|
||||
InvalidateArrange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a stamp to the display; will perform
|
||||
/// automatic layout.
|
||||
/// </summary>
|
||||
public void AddStamp(StampWidget s)
|
||||
{
|
||||
_stamps.Add(s);
|
||||
AddChild(s);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var random = new Random(PlacementSeed);
|
||||
var r = (finalSize * 0.5f).Length();
|
||||
var dtheta = -MathHelper.DegreesToRadians(90);
|
||||
var theta0 = random.Next(0, 3) * dtheta;
|
||||
var thisCenter = PixelSizeBox.TopLeft + finalSize * UIScale * 0.5f;
|
||||
|
||||
// Here's where we lay out the stamps. The first stamp goes in the
|
||||
// center of this container; subsequent stamps will chose an angle
|
||||
// (theta) to place the center of the stamp. The stamp is moved out
|
||||
// as far as it can in that direction, taking the size and
|
||||
// orientation of the stamp into account.
|
||||
for (var i = 0; i < _stamps.Count; i++)
|
||||
{
|
||||
var stampOrientation = MathHelper.DegreesToRadians((random.NextFloat() - 0.5f) * 10.0f) ;
|
||||
_stamps[i].Orientation = stampOrientation;
|
||||
|
||||
var theta = theta0 + dtheta * 0.5f + dtheta * i + (i > 4 ? MathF.Log(1 + i / 4) * dtheta : 0); // There is probably a better way to lay these out, to minimize overlaps
|
||||
var childCenterOnCircle = thisCenter;
|
||||
if (i > 0)
|
||||
{
|
||||
// First stamp can go in the center. Subsequent stamps have to find space.
|
||||
childCenterOnCircle += new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * r * UIScale;
|
||||
}
|
||||
|
||||
var childHeLocal = _stamps[i].DesiredPixelSize * 0.5f;
|
||||
var c = childHeLocal * MathF.Abs(MathF.Cos(stampOrientation));
|
||||
var s = childHeLocal * MathF.Abs(MathF.Sin(stampOrientation));
|
||||
var childHePage = new Vector2(c.X + s.Y, s.X + c.Y);
|
||||
var controlBox = new UIBox2(PixelSizeBox.TopLeft, PixelSizeBox.TopLeft + finalSize * UIScale);
|
||||
var clampedCenter = Clamp(Shrink(controlBox, childHePage), childCenterOnCircle);
|
||||
var finalPosition = clampedCenter - childHePage;
|
||||
var finalPositionAsInt = new Vector2i((int)finalPosition.X, (int)finalPosition.Y);
|
||||
_stamps[i].ArrangePixel(new UIBox2i(finalPositionAsInt, finalPositionAsInt + _stamps[i].DesiredPixelSize));
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shrink a UIBox2 by a half extents, moving both the top-left and
|
||||
/// bottom-right closer together.
|
||||
/// </summary>
|
||||
private UIBox2 Shrink(UIBox2 box, Vector2 shrinkHe)
|
||||
{
|
||||
return new UIBox2(box.TopLeft + shrinkHe, box.BottomRight - shrinkHe);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the input vector clamped to be within the UIBox
|
||||
/// </summary>
|
||||
private Vector2 Clamp(UIBox2 box, Vector2 point)
|
||||
{
|
||||
return Vector2.Min(box.BottomRight, Vector2.Max(box.TopLeft, point));
|
||||
}
|
||||
}
|
||||
|
||||
2
Content.Client/Paper/UI/StampLabel.xaml
Normal file
2
Content.Client/Paper/UI/StampLabel.xaml
Normal file
@@ -0,0 +1,2 @@
|
||||
<paper:StampLabel xmlns="https://spacestation14.io"
|
||||
xmlns:paper="clr-namespace:Content.Client.Paper.UI" />
|
||||
56
Content.Client/Paper/UI/StampLabel.xaml.cs
Normal file
56
Content.Client/Paper/UI/StampLabel.xaml.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StampLabel : Label
|
||||
{
|
||||
/// A scale that's applied to the text to ensure it
|
||||
/// fits in the allowed space.
|
||||
private Vector2 _textScaling = Vector2.One;
|
||||
|
||||
/// Shader used to draw the stamps
|
||||
private ShaderInstance? _stampShader;
|
||||
|
||||
/// Allows an additional orientation to be applied to
|
||||
/// this control.
|
||||
public float Orientation = 0.0f;
|
||||
|
||||
public StampLabel()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var prototypes = IoCManager.Resolve<IPrototypeManager>();
|
||||
_stampShader = prototypes.Index<ShaderPrototype>("PaperStamp").InstanceUnique();
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
var desiredTextSize = base.MeasureOverride(availableSize);
|
||||
var clampedScale = Vector2.Min(availableSize / desiredTextSize, Vector2.One);
|
||||
var keepAspectRatio = MathF.Min(clampedScale.X, clampedScale.Y);
|
||||
const float shimmerReduction = 0.1f;
|
||||
_textScaling = Vector2.One * MathF.Round(keepAspectRatio / shimmerReduction) * shimmerReduction;
|
||||
return desiredTextSize;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var offset = new Vector2(PixelPosition.X * MathF.Cos(Orientation) - PixelPosition.Y * MathF.Sin(Orientation),
|
||||
PixelPosition.Y * MathF.Cos(Orientation) + PixelPosition.X * MathF.Sin(Orientation));
|
||||
|
||||
_stampShader?.SetParameter("objCoord", GlobalPosition * UIScale * new Vector2(1, -1));
|
||||
handle.UseShader(_stampShader);
|
||||
handle.SetTransform(GlobalPixelPosition - PixelPosition + offset, Orientation, _textScaling);
|
||||
base.Draw(handle);
|
||||
|
||||
// Restore a sane transform+shader
|
||||
handle.SetTransform(Matrix3.Identity);
|
||||
handle.UseShader(null);
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,7 @@
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:paper="clr-namespace:Content.Client.Paper.UI" HorizontalAlignment="Center" Margin="6">
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Control MinSize="3 3" />
|
||||
</PanelContainer>
|
||||
|
||||
<Label Name="StampedByLabel" StyleClasses="LabelHeadingBigger" FontColorOverride="{x:Static style:StyleNano.DangerousRedFore}" Margin="12 6 12 6"/>
|
||||
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Control MinSize="3 3" />
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<paper:StampLabel Name="StampedByLabel" StyleClasses="LabelHeadingBigger"
|
||||
FontColorOverride="{x:Static style:StyleNano.DangerousRedFore}"
|
||||
Margin="12 6 12 6"/>
|
||||
</paper:StampWidget>
|
||||
|
||||
@@ -1,23 +1,59 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Paper.UI
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StampWidget : PanelContainer
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StampWidget : Container
|
||||
{
|
||||
public string? Stamper {
|
||||
get => StampedByLabel.Text;
|
||||
set => StampedByLabel.Text = value;
|
||||
}
|
||||
private StyleBoxTexture _borderTexture;
|
||||
private ShaderInstance? _stampShader;
|
||||
|
||||
public StampWidget()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
public float Orientation
|
||||
{
|
||||
get => StampedByLabel.Orientation;
|
||||
set => StampedByLabel.Orientation = value;
|
||||
}
|
||||
|
||||
public StampDisplayInfo StampInfo {
|
||||
set {
|
||||
StampedByLabel.Text = Loc.GetString(value.StampedName);
|
||||
StampedByLabel.FontColorOverride = value.StampedColor;
|
||||
ModulateSelfOverride = value.StampedColor;
|
||||
}
|
||||
}
|
||||
|
||||
public StampWidget()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
var resCache = IoCManager.Resolve<IResourceCache>();
|
||||
var borderImage = resCache.GetResource<TextureResource>(
|
||||
"/Textures/Interface/Paper/paper_stamp_border.svg.96dpi.png");
|
||||
_borderTexture = new StyleBoxTexture {
|
||||
Texture = borderImage,
|
||||
};
|
||||
_borderTexture.SetPatchMargin(StyleBoxTexture.Margin.All, 7.0f);
|
||||
PanelOverride = _borderTexture;
|
||||
|
||||
var prototypes = IoCManager.Resolve<IPrototypeManager>();
|
||||
_stampShader = prototypes.Index<ShaderPrototype>("PaperStamp").InstanceUnique();
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
_stampShader?.SetParameter("objCoord", GlobalPosition * UIScale * new Vector2(1, -1));
|
||||
handle.UseShader(_stampShader);
|
||||
handle.SetTransform(GlobalPosition * UIScale, Orientation, Vector2.One);
|
||||
base.Draw(handle);
|
||||
|
||||
// Restore a sane transform+shader
|
||||
handle.SetTransform(Matrix3.Identity);
|
||||
handle.UseShader(null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user