mirror of
https://github.com/BlossomiShymae/Needlework.Net.git
synced 2025-12-06 18:20:47 +01:00
Compare commits
5 Commits
0.8.0.0
...
2e4637f533
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e4637f533 | ||
|
|
7aaa79956c | ||
|
|
e9d4615ecf | ||
|
|
fb63adc1b7 | ||
|
|
b41be19cd9 |
1
Needlework.Net/Assets/libraries.json
Normal file
1
Needlework.Net/Assets/libraries.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"Repo":"GrrrLCU","Description":"A simple wrapper for the LCU. Grrr. x3","Language":"C#","Link":"https://github.com/BlossomiShymae/GrrrLCU"},{"Repo":"Kunc.RiotGames","Description":null,"Language":"C#","Link":"https://github.com/AoshiW/Kunc.RiotGames"},{"Repo":"rito","Description":"Rito is a simple, crossplatform (Windows and Linux) C++20 library interfacing with Riot services (i.e. Riot REST API and League of Legends client).","Language":"cpp","Link":"https://github.com/bartekprtc/rito"},{"Repo":"R4J","Description":"A Java library containing the API for every Riot game","Language":"Java","Link":"https://github.com/stelar7/R4J"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"JavaScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"lcu-driver","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/sousa-andre/lcu-driver"},{"Repo":"willump","Description":"Python3 helper for the League of Legends LCU API.","Language":"Python","Link":"https://github.com/elliejs/Willump"},{"Repo":"Irelia","Description":"LoL LCU Wrapper for Rust, built on top of hyper!","Language":"Rust","Link":"https://github.com/AlsoSylv/Irelia"},{"Repo":"Shaco","Description":"League of Legends LCU wrapper for rust","Language":"Rust","Link":"https://github.com/Leastrio/Shaco"},{"Repo":"hasagi-core","Description":"LCU library with auto-generated types for request parameters and responses","Language":"TypeScript","Link":"https://github.com/dysolix/hasagi-core"},{"Repo":"hexgate","Description":"LCU API wrapper for League of Legends","Language":"TypeScript","Link":"https://github.com/cuppachino/hexgate"}]
|
||||||
9
Needlework.Net/Models/Library.cs
Normal file
9
Needlework.Net/Models/Library.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Needlework.Net.Models;
|
||||||
|
|
||||||
|
public class Library
|
||||||
|
{
|
||||||
|
public required string Repo { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
public required string Language { get; init; }
|
||||||
|
public required string Link { get; init; }
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
<AvaloniaXamlIlDebuggerLaunch>False</AvaloniaXamlIlDebuggerLaunch>
|
||||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
<AssemblyName>NeedleworkDotNet</AssemblyName>
|
||||||
<AssemblyVersion>0.8.0.0</AssemblyVersion>
|
<AssemblyVersion>0.9.0.0</AssemblyVersion>
|
||||||
<FileVersion>$(AssemblyVersion)</FileVersion>
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
<AvaloniaXamlVerboseExceptions>False</AvaloniaXamlVerboseExceptions>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public partial class EndpointsViewModel : ObservableObject
|
|||||||
|
|
||||||
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked)
|
public EndpointsViewModel(IAvaloniaList<string> plugins, Action<ObservableObject> onClicked)
|
||||||
{
|
{
|
||||||
Plugins = plugins;
|
Plugins = new AvaloniaList<string>(plugins);
|
||||||
Query = plugins;
|
Query = new AvaloniaList<string>(plugins);
|
||||||
OnClicked = onClicked;
|
OnClicked = onClicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using Avalonia.Platform;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Needlework.Net.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Needlework.Net.ViewModels.Pages;
|
namespace Needlework.Net.ViewModels.Pages;
|
||||||
|
|
||||||
public partial class HomeViewModel : PageBase
|
public partial class HomeViewModel : PageBase
|
||||||
{
|
{
|
||||||
|
public List<Library> Libraries { get; } = JsonSerializer.Deserialize<List<Library>>(AssetLoader.Open(new Uri($"avares://NeedleworkDotNet/Assets/libraries.json")))!;
|
||||||
|
|
||||||
public HomeViewModel() : base("Home", "home", int.MinValue) { }
|
public HomeViewModel() : base("Home", "home", int.MinValue) { }
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class EventViewModel : ObservableObject
|
|||||||
public EventViewModel(EventData eventData)
|
public EventViewModel(EventData eventData)
|
||||||
{
|
{
|
||||||
Time = $"{DateTime.Now:HH:mm:ss.fff}";
|
Time = $"{DateTime.Now:HH:mm:ss.fff}";
|
||||||
Type = eventData?.EventType.ToUpper() ?? string.Empty;
|
Type = eventData?.EventType?.ToUpper() ?? string.Empty;
|
||||||
Uri = eventData?.Uri ?? string.Empty;
|
Uri = eventData?.Uri ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BlossomiShymae.GrrrLCU;
|
using Avalonia.Collections;
|
||||||
|
using BlossomiShymae.GrrrLCU;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
@@ -8,8 +9,11 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Websocket.Client;
|
using Websocket.Client;
|
||||||
|
|
||||||
namespace Needlework.Net.ViewModels.Pages.Websocket;
|
namespace Needlework.Net.ViewModels.Pages.Websocket;
|
||||||
@@ -25,38 +29,77 @@ public partial class WebsocketViewModel : PageBase
|
|||||||
[ObservableProperty] private bool _isTail = false;
|
[ObservableProperty] private bool _isTail = false;
|
||||||
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
|
[ObservableProperty] private EventViewModel? _selectedEventLog = null;
|
||||||
|
|
||||||
|
[ObservableProperty] private IAvaloniaList<string> _eventTypes = new AvaloniaList<string>();
|
||||||
|
[ObservableProperty] private string _eventType = "OnJsonApiEvent";
|
||||||
|
|
||||||
private Dictionary<string, EventMessage> _events = [];
|
private Dictionary<string, EventMessage> _events = [];
|
||||||
|
|
||||||
public WebsocketClient? Client { get; set; }
|
public WebsocketClient? Client { get; set; }
|
||||||
|
|
||||||
|
public List<IDisposable> ClientDisposables = [];
|
||||||
|
|
||||||
|
private readonly object _tokenLock = new();
|
||||||
|
public CancellationTokenSource TokenSource { get; set; } = new();
|
||||||
|
|
||||||
|
public HttpClient HttpClient { get; }
|
||||||
|
|
||||||
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
public IReadOnlyList<EventViewModel> FilteredEventLog => string.IsNullOrWhiteSpace(Search) ? EventLog : [.. EventLog.Where(x => x.Key.Contains(Search, StringComparison.InvariantCultureIgnoreCase))];
|
||||||
|
|
||||||
public WebsocketViewModel() : base("Event Viewer", "plug", -100)
|
public WebsocketViewModel(HttpClient httpClient) : base("Event Viewer", "plug", -100)
|
||||||
{
|
{
|
||||||
|
HttpClient = httpClient;
|
||||||
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
EventLog.CollectionChanged += (s, e) => OnPropertyChanged(nameof(FilteredEventLog));
|
||||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
Task.Run(async () =>
|
||||||
thread.Start();
|
{
|
||||||
|
await InitializeEventTypes();
|
||||||
|
InitializeWebsocket();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeEventTypes()
|
||||||
|
{
|
||||||
|
var file = await HttpClient.GetStringAsync("https://raw.githubusercontent.com/dysolix/hasagi-types/refs/heads/main/lcu-events.d.ts");
|
||||||
|
var matches = EventTypesRegex().Matches(file);
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Invoke(() => EventTypes.AddRange(matches.Select(m => m.Groups[1].Value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeWebsocket()
|
private void InitializeWebsocket()
|
||||||
{
|
{
|
||||||
while (true)
|
lock (_tokenLock)
|
||||||
|
{
|
||||||
|
if (Client != null)
|
||||||
|
{
|
||||||
|
foreach (var disposable in ClientDisposables)
|
||||||
|
disposable.Dispose();
|
||||||
|
ClientDisposables.Clear();
|
||||||
|
Client.Dispose();
|
||||||
|
}
|
||||||
|
TokenSource.Cancel();
|
||||||
|
var tokenSource = new CancellationTokenSource();
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
while (!tokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = Connector.CreateLcuWebsocketClient();
|
var client = Connector.CreateLcuWebsocketClient();
|
||||||
client.EventReceived.Subscribe(OnMessage);
|
ClientDisposables.Add(client.EventReceived.Subscribe(OnMessage));
|
||||||
client.DisconnectionHappened.Subscribe(OnDisconnection);
|
ClientDisposables.Add(client.DisconnectionHappened.Subscribe(OnDisconnection));
|
||||||
client.ReconnectionHappened.Subscribe(OnReconnection);
|
ClientDisposables.Add(client.ReconnectionHappened.Subscribe(OnReconnection));
|
||||||
|
|
||||||
client.Start();
|
client.Start();
|
||||||
client.Send(new EventMessage(EventRequestType.Subscribe, EventKinds.OnJsonApiEvent));
|
client.Send(new EventMessage(EventRequestType.Subscribe, new EventKind() { Prefix = EventType }));
|
||||||
Client = client;
|
Client = client;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception) { }
|
catch (Exception) { }
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
{ IsBackground = true };
|
||||||
|
thread.Start();
|
||||||
|
TokenSource = tokenSource;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedEventLogChanged(EventViewModel? value)
|
partial void OnSelectedEventLogChanged(EventViewModel? value)
|
||||||
@@ -84,10 +127,13 @@ public partial class WebsocketViewModel : PageBase
|
|||||||
|
|
||||||
private void OnDisconnection(DisconnectionInfo info)
|
private void OnDisconnection(DisconnectionInfo info)
|
||||||
{
|
{
|
||||||
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
|
Trace.WriteLine($"-- Disconnection --\nType:{info.Type}\nSubProtocol:{info.SubProtocol}\nCloseStatus:{info.CloseStatus}\nCloseStatusDescription:{info.CloseStatusDescription}\nExceptionMessage:{info?.Exception?.Message}\n:InnerException:{info?.Exception?.InnerException}");
|
||||||
Client?.Dispose();
|
InitializeWebsocket();
|
||||||
var thread = new Thread(InitializeWebsocket) { IsBackground = true };
|
}
|
||||||
thread.Start();
|
|
||||||
|
partial void OnEventTypeChanged(string value)
|
||||||
|
{
|
||||||
|
InitializeWebsocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMessage(EventMessage message)
|
private void OnMessage(EventMessage message)
|
||||||
@@ -122,4 +168,7 @@ public partial class WebsocketViewModel : PageBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("\"(.*?)\":")]
|
||||||
|
public static partial Regex EventTypesRegex();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
<TransitioningContentControl Content="{Binding CurrentPage}"/>
|
||||||
<Button Content="{Binding Version}"
|
<Button Content="{Binding Version}"
|
||||||
|
Background="RoyalBlue"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Margin="16"/>
|
Margin="16"/>
|
||||||
|
|||||||
@@ -5,10 +5,32 @@
|
|||||||
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
|
xmlns:vm="using:Needlework.Net.ViewModels.Pages"
|
||||||
xmlns:controls="using:Needlework.Net.Controls"
|
xmlns:controls="using:Needlework.Net.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
Name="HomeControl"
|
||||||
x:Class="Needlework.Net.Views.Pages.HomeView"
|
x:Class="Needlework.Net.Views.Pages.HomeView"
|
||||||
x:DataType="vm:HomeViewModel">
|
x:DataType="vm:HomeViewModel">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGrid">
|
||||||
|
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ControlElevationBorderBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridColumnHeader TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridRow DataGridCell">
|
||||||
|
<Setter Property="FontSize" Value="12"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="DataGridRow">
|
||||||
|
<Setter Property="Margin" Value="0 0 0 4"></Setter>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
<!-- TOP LEVEL -->
|
<!-- TOP LEVEL -->
|
||||||
<ScrollViewer>
|
<Grid ColumnDefinitions="*,400"
|
||||||
|
RowDefinitions="*">
|
||||||
|
<!-- MAIN AREA -->
|
||||||
|
<ScrollViewer Grid.Column="0"
|
||||||
|
Grid.Row="0">
|
||||||
<WrapPanel Margin="8"
|
<WrapPanel Margin="8"
|
||||||
Orientation="Horizontal">
|
Orientation="Horizontal">
|
||||||
<!-- WELCOME -->
|
<!-- WELCOME -->
|
||||||
@@ -32,12 +54,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
Theme="{StaticResource SubtitleTextBlockStyle}"
|
Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||||
Margin="0 0 0 8">Resources</TextBlock>
|
Margin="0 0 0 8">Resources</TextBlock>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
<StackPanel.Styles>
|
|
||||||
<Style Selector="Button">
|
|
||||||
<Setter Property="Command" Value="{Binding OpenUrlCommand}"/>
|
|
||||||
</Style>
|
|
||||||
</StackPanel.Styles>
|
|
||||||
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 8 0">
|
<Button CommandParameter="https://hextechdocs.dev/tag/lcu/" Margin="0 0 8 0">
|
||||||
Hextech Docs
|
Hextech Docs
|
||||||
</Button>
|
</Button>
|
||||||
@@ -60,4 +77,44 @@
|
|||||||
</controls:Card>
|
</controls:Card>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
<!-- LIBRARIES -->
|
||||||
|
<Grid Margin="20"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
ColumnDefinitions="*"
|
||||||
|
RowDefinitions="auto,*">
|
||||||
|
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0">Libraries</TextBlock>
|
||||||
|
<ScrollViewer Grid.Column="0"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<ItemsRepeater ItemsSource="{Binding Libraries}">
|
||||||
|
<ItemsRepeater.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="0 12 0 0">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text="{Binding Language}"
|
||||||
|
FontWeight="Bold"/>
|
||||||
|
<Bold> - </Bold>
|
||||||
|
<Run Text="{Binding Repo}"
|
||||||
|
FontWeight="Bold"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding Description}"
|
||||||
|
IsVisible="{Binding Description, Converter={StaticResource NullBoolConverter}}"
|
||||||
|
TextAlignment="Left"
|
||||||
|
TextWrapping="WrapWithOverflow"
|
||||||
|
Width="350"/>
|
||||||
|
<Button Command="{Binding #HomeControl.((vm:HomeViewModel)DataContext).OpenUrlCommand}"
|
||||||
|
CommandParameter="{Binding Link}"
|
||||||
|
Margin="0 4 0 0">
|
||||||
|
<TextBlock Text="{Binding Link}"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsRepeater.ItemTemplate>
|
||||||
|
</ItemsRepeater>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -10,12 +10,22 @@
|
|||||||
<Grid RowDefinitions="*,auto,*" Margin="16">
|
<Grid RowDefinitions="*,auto,*" Margin="16">
|
||||||
<Border Grid.Row="0"
|
<Border Grid.Row="0"
|
||||||
Padding="0 0 0 8">
|
Padding="0 0 0 8">
|
||||||
<Grid RowDefinitions="auto,*" ColumnDefinitions="*">
|
<Grid RowDefinitions="auto,auto,*" ColumnDefinitions="*">
|
||||||
<Grid
|
<Grid Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
RowDefinitions="*">
|
||||||
|
<ComboBox ItemsSource="{Binding EventTypes}"
|
||||||
|
SelectedItem="{Binding EventType}"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Left"/>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
RowDefinitions="*"
|
RowDefinitions="*"
|
||||||
ColumnDefinitions="auto,*,auto,auto">
|
ColumnDefinitions="auto,*,auto,auto"
|
||||||
|
Margin="0 8 0 0">
|
||||||
<Button Margin="0 0 8 0"
|
<Button Margin="0 0 8 0"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -37,7 +47,7 @@
|
|||||||
Content="Tail"
|
Content="Tail"
|
||||||
IsChecked="{Binding IsTail}"/>
|
IsChecked="{Binding IsTail}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ListBox Grid.Row="1"
|
<ListBox Grid.Row="2"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Name="EventViewer"
|
Name="EventViewer"
|
||||||
Margin="0 8 0 0"
|
Margin="0 8 0 0"
|
||||||
|
|||||||
Reference in New Issue
Block a user