wired up key commands
This commit is contained in:
+2
-2
@@ -3,8 +3,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Hyperbar.Widget.Contextual;
|
||||
|
||||
public class ContextualWidgetBuilder :
|
||||
IWidgetBuilder
|
||||
public class ContextualWidgetProvider :
|
||||
IWidgetProvider
|
||||
{
|
||||
public void Create(IServiceCollection services) => services
|
||||
.AddConfiguration<ContextualWidgetConfiguration>()
|
||||
@@ -2,7 +2,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Hyperbar.Windows.Controls</RootNamespace>
|
||||
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<UseRidGraph>true</UseRidGraph>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Hyperbar.Windows.Primary;
|
||||
|
||||
public interface IPrimaryCommand
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Hyperbar.Windows.Primary;
|
||||
|
||||
public interface IPrimaryCommandConfiguration
|
||||
{
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
namespace Hyperbar.Windows.Primary;
|
||||
|
||||
public class KeyAcceleratorCommand :
|
||||
IPrimaryCommand
|
||||
public class KeyAcceleratorCommandConfiguration :
|
||||
IPrimaryCommandConfiguration
|
||||
{
|
||||
public string? Icon { get; set; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Hyperbar.Windows.Primary;
|
||||
|
||||
public class PrimaryWidgetConfiguration :
|
||||
List<IPrimaryCommand>
|
||||
List<IPrimaryCommandConfiguration>
|
||||
{
|
||||
}
|
||||
+2
-2
@@ -3,8 +3,8 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Hyperbar.Windows.Primary;
|
||||
|
||||
public class PrimaryWidgetBuilder :
|
||||
IWidgetBuilder
|
||||
public class PrimaryWidgetProvider :
|
||||
IWidgetProvider
|
||||
{
|
||||
public void Create(IServiceCollection services) => services.AddConfiguration<PrimaryWidgetConfiguration>()
|
||||
.AddWidgetTemplate<PrimaryWidgetViewModel>();
|
||||
@@ -4,15 +4,13 @@ public class PrimaryWidgetViewModel :
|
||||
WidgetViewModelBase
|
||||
{
|
||||
public PrimaryWidgetViewModel(ITemplateFactory templateFactory,
|
||||
IServiceFactory serviceFactory) : base(templateFactory, serviceFactory)
|
||||
IServiceFactory serviceFactory,
|
||||
IMediator mediator) : base(templateFactory, serviceFactory)
|
||||
{
|
||||
;
|
||||
Add<WidgetButtonViewModel>("Start", new Action(() => mediator.Send(new KeyAcceleratorCommand(VirtualKey.LeftWindows))));
|
||||
|
||||
Add<WidgetButtonViewModel>("test 1", new Action(() =>
|
||||
{
|
||||
}));
|
||||
Add<WidgetButtonViewModel>("test 2", new Action(() => { }));
|
||||
Add<WidgetButtonViewModel>("test 4", new Action(() => { }));
|
||||
Add<WidgetButtonViewModel>("test 5", new Action(() => { }));
|
||||
//Add<WidgetButtonViewModel>("test 2", new Action(() => { }));
|
||||
//Add<WidgetButtonViewModel>("test 4", new Action(() => { }));
|
||||
//Add<WidgetButtonViewModel>("test 5", new Action(() => { }));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Hyperbar.Desktop.Win32</RootNamespace>
|
||||
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<UseRidGraph>true</UseRidGraph>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Hyperbar.Windows.Win32
|
||||
{
|
||||
public interface IVirtualKeyboard
|
||||
{
|
||||
void Send(int key, params int[] modifierKeys);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,35 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.System;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.UI.KeyboardAndMouseInput;
|
||||
|
||||
namespace Hyperbar.Windows.Win32;
|
||||
|
||||
public class KeyInterop
|
||||
public class VirtualKeyboard :
|
||||
IVirtualKeyboard
|
||||
{
|
||||
private static readonly VirtualKey[] extendedKeys = [
|
||||
VirtualKey.Menu,
|
||||
VirtualKey.LeftMenu,
|
||||
VirtualKey.NumberKeyLock,
|
||||
VirtualKey.Insert,
|
||||
VirtualKey.Delete,
|
||||
VirtualKey.Home,
|
||||
VirtualKey.End,
|
||||
VirtualKey.Up,
|
||||
VirtualKey.Down,
|
||||
VirtualKey.Left,
|
||||
VirtualKey.Right,
|
||||
VirtualKey.Application,
|
||||
VirtualKey.RightWindows,
|
||||
VirtualKey.LeftWindows];
|
||||
private readonly int[] extendedKeys = [
|
||||
165, // RightMenu
|
||||
164, // LeftMenu
|
||||
144, // NumberKeyLock
|
||||
45, // Insert
|
||||
46, // Delete
|
||||
36, // Home
|
||||
35, // End
|
||||
36, // Up,
|
||||
40, // Down,
|
||||
37, // Left
|
||||
39, // Right,
|
||||
93, // Application,
|
||||
92, // RightWindows
|
||||
91 // LeftWindows
|
||||
];
|
||||
|
||||
public static void Send(VirtualKey key,
|
||||
params VirtualKey[] modifierKeys)
|
||||
public void Send(int key,
|
||||
params int[] modifierKeys)
|
||||
{
|
||||
foreach (VirtualKey modiferKey in modifierKeys)
|
||||
foreach (int modiferKey in modifierKeys)
|
||||
{
|
||||
Press(modiferKey);
|
||||
}
|
||||
@@ -36,17 +37,17 @@ public class KeyInterop
|
||||
Press(key);
|
||||
Release(key);
|
||||
|
||||
foreach (VirtualKey modifierKey in modifierKeys.Reverse())
|
||||
foreach (int modifierKey in modifierKeys.Reverse())
|
||||
{
|
||||
Release(modifierKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Press(VirtualKey key) => Send(key, true);
|
||||
private void Press(int key) => Send(key, true);
|
||||
|
||||
private static void Release(VirtualKey key) => Send(key, false);
|
||||
private void Release(int key) => Send(key, false);
|
||||
|
||||
private static unsafe void Send(VirtualKey key,
|
||||
private unsafe void Send(int key,
|
||||
bool pressed)
|
||||
{
|
||||
INPUT input = new()
|
||||
@@ -69,7 +70,7 @@ public class KeyInterop
|
||||
flags |= KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
if (extendedKeys.Contains(key))
|
||||
if (extendedKeys.Contains((int)key))
|
||||
{
|
||||
flags |= KEYBD_EVENT_FLAGS.KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
@@ -40,8 +40,8 @@ public partial class App :
|
||||
services.AddTransient<DesktopFlyout>();
|
||||
services.AddContentTemplate<CommandViewModel, CommandView>();
|
||||
|
||||
services.AddWidget<ContextualWidgetBuilder>();
|
||||
services.AddWidget<PrimaryWidgetBuilder>();
|
||||
services.AddWidgetProvider<ContextualWidgetProvider>();
|
||||
services.AddWidgetProvider<PrimaryWidgetProvider>();
|
||||
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Hyperbar.Windows.Win32;
|
||||
|
||||
namespace Hyperbar.Windows;
|
||||
|
||||
public class KeyAcceleratorCommandHandler(IVirtualKeyboard virtualKeyboard) :
|
||||
ICommandHandler<KeyAcceleratorCommand>
|
||||
{
|
||||
public ValueTask<Unit> Handle(KeyAcceleratorCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{ virtualKeyboard.Send((int)command.Key);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Hyperbar.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Hyperbar.Extensions;
|
||||
using Hyperbar.Windows.Win32;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
@@ -6,17 +7,21 @@ namespace Hyperbar.Windows
|
||||
{
|
||||
public static class IServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddWidget<TCommandBuilder>(this IServiceCollection services)
|
||||
where TCommandBuilder :
|
||||
IWidgetBuilder, new()
|
||||
public static IServiceCollection AddWidgetProvider<TWidgetProvider>(this IServiceCollection services)
|
||||
where TWidgetProvider :
|
||||
IWidgetProvider, new()
|
||||
{
|
||||
TCommandBuilder builder = new();
|
||||
TWidgetProvider builder = new();
|
||||
IHost? host = new HostBuilder()
|
||||
.ConfigureServices(isolatedServices =>
|
||||
{
|
||||
|
||||
isolatedServices.AddSingleton<IServiceFactory>(provider =>
|
||||
new ServiceFactory((type, parameters) => ActivatorUtilities.CreateInstance(provider, type, parameters!)));
|
||||
|
||||
isolatedServices.AddSingleton<IVirtualKeyboard, VirtualKeyboard>();
|
||||
|
||||
isolatedServices.AddSingleton<IMediator, Mediator>();
|
||||
isolatedServices.AddHandler<KeyAcceleratorCommandHandler>();
|
||||
|
||||
isolatedServices.AddTransient<IWidgetView, WidgetView>();
|
||||
isolatedServices.AddContentTemplate<WidgetButtonViewModel, WidgetButtonView>();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Hyperbar.Windows;
|
||||
|
||||
public class KeyAcceleratorCommandHandler :
|
||||
ICommandHandler<KeyAcceleratorCommand>
|
||||
{
|
||||
public ValueTask<Unit> Handle(KeyAcceleratorCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Hyperbar;
|
||||
|
||||
public record KeyAcceleratorCommand(VirtualKey Key,
|
||||
VirtualKey[]? Modifiers = null) :
|
||||
ICommand;
|
||||
@@ -0,0 +1,175 @@
|
||||
namespace Hyperbar;
|
||||
|
||||
public enum VirtualKey
|
||||
{
|
||||
None = 0,
|
||||
LeftButton = 1,
|
||||
RightButton = 2,
|
||||
Cancel = 3,
|
||||
MiddleButton = 4,
|
||||
XButton1 = 5,
|
||||
XButton2 = 6,
|
||||
Back = 8,
|
||||
Tab = 9,
|
||||
Clear = 12,
|
||||
Enter = 13,
|
||||
Shift = 16,
|
||||
Control = 17,
|
||||
Menu = 18,
|
||||
Pause = 19,
|
||||
CapitalLock = 20,
|
||||
Kana = 21,
|
||||
Hangul = 21,
|
||||
Junja = 23,
|
||||
Final = 24,
|
||||
Hanja = 25,
|
||||
Kanji = 25,
|
||||
Escape = 27,
|
||||
Convert = 28,
|
||||
NonConvert = 29,
|
||||
Accept = 30,
|
||||
ModeChange = 31,
|
||||
Space = 32,
|
||||
PageUp = 33,
|
||||
PageDown = 34,
|
||||
End = 35,
|
||||
Home = 36,
|
||||
Left = 37,
|
||||
Up = 38,
|
||||
Right = 39,
|
||||
Down = 40,
|
||||
Select = 41,
|
||||
Print = 42,
|
||||
Execute = 43,
|
||||
Snapshot = 44,
|
||||
Insert = 45,
|
||||
Delete = 46,
|
||||
Help = 47,
|
||||
Number0 = 48,
|
||||
Number1 = 49,
|
||||
Number2 = 50,
|
||||
Number3 = 51,
|
||||
Number4 = 52,
|
||||
Number5 = 53,
|
||||
Number6 = 54,
|
||||
Number7 = 55,
|
||||
Number8 = 56,
|
||||
Number9 = 57,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
LeftWindows = 91,
|
||||
RightWindows = 92,
|
||||
Application = 93,
|
||||
Sleep = 95,
|
||||
NumberPad0 = 96,
|
||||
NumberPad1 = 97,
|
||||
NumberPad2 = 98,
|
||||
NumberPad3 = 99,
|
||||
NumberPad4 = 100,
|
||||
NumberPad5 = 101,
|
||||
NumberPad6 = 102,
|
||||
NumberPad7 = 103,
|
||||
NumberPad8 = 104,
|
||||
NumberPad9 = 105,
|
||||
Multiply = 106,
|
||||
Add = 107,
|
||||
Separator = 108,
|
||||
Subtract = 109,
|
||||
Decimal = 110,
|
||||
Divide = 111,
|
||||
F1 = 112,
|
||||
F2 = 113,
|
||||
F3 = 114,
|
||||
F4 = 115,
|
||||
F5 = 116,
|
||||
F6 = 117,
|
||||
F7 = 118,
|
||||
F8 = 119,
|
||||
F9 = 120,
|
||||
F10 = 121,
|
||||
F11 = 122,
|
||||
F12 = 123,
|
||||
F13 = 124,
|
||||
F14 = 125,
|
||||
F15 = 126,
|
||||
F16 = 127,
|
||||
F17 = 128,
|
||||
F18 = 129,
|
||||
F19 = 130,
|
||||
F20 = 131,
|
||||
F21 = 132,
|
||||
F22 = 133,
|
||||
F23 = 134,
|
||||
F24 = 135,
|
||||
NavigationView = 136,
|
||||
NavigationMenu = 137,
|
||||
NavigationUp = 138,
|
||||
NavigationDown = 139,
|
||||
NavigationLeft = 140,
|
||||
NavigationRight = 141,
|
||||
NavigationAccept = 142,
|
||||
NavigationCancel = 143,
|
||||
NumberKeyLock = 144,
|
||||
Scroll = 145,
|
||||
LeftShift = 160,
|
||||
RightShift = 161,
|
||||
LeftControl = 162,
|
||||
RightControl = 163,
|
||||
LeftMenu = 164,
|
||||
RightMenu = 165,
|
||||
GoBack = 166,
|
||||
GoForward = 167,
|
||||
Refresh = 168,
|
||||
Stop = 169,
|
||||
Search = 170,
|
||||
Favorites = 171,
|
||||
GoHome = 172,
|
||||
GamepadA = 195,
|
||||
GamepadB = 196,
|
||||
GamepadX = 197,
|
||||
GamepadY = 198,
|
||||
GamepadRightShoulder = 199,
|
||||
GamepadLeftShoulder = 200,
|
||||
GamepadLeftTrigger = 201,
|
||||
GamepadRightTrigger = 202,
|
||||
GamepadDPadUp = 203,
|
||||
GamepadDPadDown = 204,
|
||||
GamepadDPadLeft = 205,
|
||||
GamepadDPadRight = 206,
|
||||
GamepadMenu = 207,
|
||||
GamepadView = 208,
|
||||
GamepadLeftThumbstickButton = 209,
|
||||
GamepadRightThumbstickButton = 210,
|
||||
GamepadLeftThumbstickUp = 211,
|
||||
GamepadLeftThumbstickDown = 212,
|
||||
GamepadLeftThumbstickRight = 213,
|
||||
GamepadLeftThumbstickLeft = 214,
|
||||
GamepadRightThumbstickUp = 215,
|
||||
GamepadRightThumbstickDown = 216,
|
||||
GamepadRightThumbstickRight = 217,
|
||||
GamepadRightThumbstickLeft = 218
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public class CommandClassHandlerWrapper<TRequest, TResponse>
|
||||
{
|
||||
MessageHandlerDelegate<TRequest, TResponse> handlerCopy = handler;
|
||||
IPipelineBehavior<TRequest, TResponse> pipelineCopy = pipeline;
|
||||
|
||||
handler = (TRequest message, CancellationToken cancellationToken) =>
|
||||
pipelineCopy.Handle(message, handlerCopy, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
public interface IMediator
|
||||
{
|
||||
ValueTask Publish<TNotification>(TNotification notification,
|
||||
ValueTask PublishAsync<TNotification>(TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification :
|
||||
INotification;
|
||||
|
||||
ValueTask<TResponse> Send<TResponse>(IRequest<TResponse> request,
|
||||
void Send<TResponse>(ICommand<TResponse> command);
|
||||
|
||||
ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<TResponse> Send<TResponse>(ICommand<TResponse> command,
|
||||
ValueTask<TResponse> SendAsync<TResponse>(ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<TResponse> Send<TResponse>(IQuery<TResponse> query,
|
||||
ValueTask<TResponse> SendAsync<TResponse>(IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
ValueTask<object?> Send(object message, CancellationToken
|
||||
ValueTask<object?> SendAsync(object message, CancellationToken
|
||||
cancellationToken = default);
|
||||
|
||||
void Subscribe(object subscriber);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Hyperbar;
|
||||
|
||||
public record KeyAcceleratorCommand(string Key,
|
||||
string[]? Modifiers = null) :
|
||||
ICommand;
|
||||
@@ -8,7 +8,7 @@ public class Mediator(IServiceProvider provider) :
|
||||
{
|
||||
private readonly ConditionalWeakTable<Type, dynamic> handlers = [];
|
||||
|
||||
public ValueTask Publish<TNotification>(TNotification notification,
|
||||
public ValueTask PublishAsync<TNotification>(TNotification notification,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TNotification :
|
||||
INotification
|
||||
@@ -36,7 +36,7 @@ public class Mediator(IServiceProvider provider) :
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<TResponse> Send<TResponse>(IRequest<TResponse> request,
|
||||
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
dynamic? handler = provider.GetService(typeof(RequestClassHandlerWrapper<,>)
|
||||
@@ -50,7 +50,7 @@ public class Mediator(IServiceProvider provider) :
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<TResponse> Send<TResponse>(ICommand<TResponse> command,
|
||||
public ValueTask<TResponse> SendAsync<TResponse>(ICommand<TResponse> command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
dynamic? handler = provider.GetService(typeof(CommandClassHandlerWrapper<,>)
|
||||
@@ -64,7 +64,18 @@ public class Mediator(IServiceProvider provider) :
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<TResponse> Send<TResponse>(IQuery<TResponse> query,
|
||||
public void Send<TResponse>(ICommand<TResponse> command)
|
||||
{
|
||||
dynamic? handler = provider.GetService(typeof(CommandClassHandlerWrapper<,>)
|
||||
.MakeGenericType(command.GetType(), typeof(TResponse)));
|
||||
|
||||
if (handler is not null)
|
||||
{
|
||||
_ = handler.Handle((dynamic)command, default(CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<TResponse> SendAsync<TResponse>(IQuery<TResponse> query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
dynamic? handler = provider.GetService(typeof(QueryClassHandlerWrapper<,>)
|
||||
@@ -78,7 +89,7 @@ public class Mediator(IServiceProvider provider) :
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<object?> Send(object message,
|
||||
public ValueTask<object?> SendAsync(object message,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.GetType().GetInterface(typeof(IRequest<>).Name) is { } requestType)
|
||||
|
||||
@@ -16,6 +16,7 @@ public class QueryClassHandlerWrapper<TRequest, TResponse>
|
||||
{
|
||||
MessageHandlerDelegate<TRequest, TResponse> handlerCopy = handler;
|
||||
IPipelineBehavior<TRequest, TResponse> pipelineCopy = pipeline;
|
||||
|
||||
handler = (TRequest message, CancellationToken cancellationToken) =>
|
||||
pipelineCopy.Handle(message, handlerCopy, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Hyperbar;
|
||||
|
||||
public interface IWidgetBuilder
|
||||
public interface IWidgetProvider
|
||||
{
|
||||
void Create(IServiceCollection services);
|
||||
}
|
||||
Reference in New Issue
Block a user