LobbyUI fixes (#27033)
* LobbyUI fixes I have no idea which were bugs prior but anyway fix stuff. * More fixes * Test moment
This commit is contained in:
@@ -64,13 +64,19 @@ namespace Content.Client.Lobby
|
||||
|
||||
_characterSetup.CloseButton.OnPressed += _ =>
|
||||
{
|
||||
// Reset sliders etc.
|
||||
_characterSetup?.UpdateControls();
|
||||
|
||||
var controller = _userInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.SetClothes(true);
|
||||
controller.UpdateProfile();
|
||||
_lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
|
||||
};
|
||||
|
||||
_characterSetup.SaveButton.OnPressed += _ =>
|
||||
{
|
||||
_characterSetup.Save();
|
||||
_userInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
|
||||
_userInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
|
||||
};
|
||||
|
||||
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Client.Humanoid;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Preferences;
|
||||
using Content.Client.Preferences.UI;
|
||||
using Content.Client.Station;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -30,6 +31,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
|
||||
private LobbyCharacterPreviewPanel? _previewPanel;
|
||||
|
||||
private bool _showClothes = true;
|
||||
|
||||
/*
|
||||
* Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
|
||||
* that is shared too.
|
||||
@@ -41,13 +44,15 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
private EntityUid? _previewDummy;
|
||||
|
||||
/// <summary>
|
||||
/// If we currently have a loadout selected.
|
||||
/// If we currently have a job prototype selected.
|
||||
/// </summary>
|
||||
private JobPrototype? _dummyJob;
|
||||
|
||||
// TODO: Load the species directly and don't update entity ever.
|
||||
public event Action<EntityUid>? PreviewDummyUpdated;
|
||||
|
||||
private HumanoidCharacterProfile? _profile;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -56,7 +61,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
|
||||
private void PreferencesDataLoaded()
|
||||
{
|
||||
UpdateCharacterUI();
|
||||
UpdateProfile();
|
||||
}
|
||||
|
||||
public void OnStateEntered(LobbyState state)
|
||||
@@ -72,44 +77,102 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
|
||||
{
|
||||
_previewPanel = panel;
|
||||
UpdateCharacterUI();
|
||||
ReloadProfile();
|
||||
}
|
||||
|
||||
public void SetClothes(bool value)
|
||||
{
|
||||
if (_showClothes == value)
|
||||
return;
|
||||
|
||||
_showClothes = value;
|
||||
ReloadCharacterUI();
|
||||
}
|
||||
|
||||
public void SetDummyJob(JobPrototype? job)
|
||||
{
|
||||
_dummyJob = job;
|
||||
UpdateCharacterUI();
|
||||
ReloadCharacterUI();
|
||||
}
|
||||
|
||||
public void UpdateCharacterUI()
|
||||
/// <summary>
|
||||
/// Updates the character only with the specified profile change.
|
||||
/// </summary>
|
||||
public void ReloadProfile()
|
||||
{
|
||||
// Test moment
|
||||
if (_stateManager.CurrentState is not LobbyState)
|
||||
if (_profile == null || _stateManager.CurrentState is not LobbyState)
|
||||
return;
|
||||
|
||||
// Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
|
||||
var previewDummy = EnsurePreviewDummy(_profile);
|
||||
_humanoid.LoadProfile(previewDummy, _profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the currently selected character's preview.
|
||||
/// </summary>
|
||||
public void ReloadCharacterUI()
|
||||
{
|
||||
// Test moment
|
||||
if (_profile == null || _stateManager.CurrentState is not LobbyState)
|
||||
return;
|
||||
|
||||
EntityManager.DeleteEntity(_previewDummy);
|
||||
_previewDummy = null;
|
||||
_previewDummy = EnsurePreviewDummy(_profile);
|
||||
_previewPanel?.SetSprite(_previewDummy.Value);
|
||||
_previewPanel?.SetSummaryText(_profile.Summary);
|
||||
_humanoid.LoadProfile(_previewDummy.Value, _profile);
|
||||
|
||||
if (_showClothes)
|
||||
GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates character profile to the default.
|
||||
/// </summary>
|
||||
public void UpdateProfile()
|
||||
{
|
||||
if (!_preferencesManager.ServerDataLoaded)
|
||||
{
|
||||
_previewPanel?.SetLoaded(false);
|
||||
_profile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_previewPanel?.SetLoaded(true);
|
||||
|
||||
if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
|
||||
if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
|
||||
{
|
||||
_previewPanel?.SetSummaryText(string.Empty);
|
||||
_profile = selectedCharacter;
|
||||
_previewPanel?.SetLoaded(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityManager.DeleteEntity(_previewDummy);
|
||||
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
|
||||
_previewPanel?.SetSprite(_previewDummy.Value);
|
||||
_previewPanel?.SetSummaryText(selectedCharacter.Summary);
|
||||
_humanoid.LoadProfile(_previewDummy.Value, selectedCharacter);
|
||||
|
||||
GiveDummyJobClothesLoadout(_previewDummy.Value, selectedCharacter);
|
||||
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
|
||||
_previewPanel?.SetSummaryText(string.Empty);
|
||||
_previewPanel?.SetLoaded(false);
|
||||
}
|
||||
|
||||
ReloadCharacterUI();
|
||||
}
|
||||
|
||||
public void UpdateProfile(HumanoidCharacterProfile? profile)
|
||||
{
|
||||
if (_profile?.Equals(profile) == true)
|
||||
return;
|
||||
|
||||
if (_stateManager.CurrentState is not LobbyState)
|
||||
return;
|
||||
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile)
|
||||
{
|
||||
if (_previewDummy != null)
|
||||
return _previewDummy.Value;
|
||||
|
||||
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(profile.Species).DollPrototype, MapCoordinates.Nullspace);
|
||||
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
|
||||
return _previewDummy.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -97,9 +97,14 @@ namespace Content.Client.Preferences.UI
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void UpdateControls()
|
||||
{
|
||||
// Reset sliders etc. upon going going back to GUI.
|
||||
_humanoidProfileEditor.LoadServerData();
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
|
||||
var numberOfFullSlots = 0;
|
||||
var characterButtonsGroup = new ButtonGroup();
|
||||
Characters.RemoveAllChildren();
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace Content.Client.Preferences.UI
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairStyleName(newStyle.id));
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_hairPicker.OnColorChanged += newColor =>
|
||||
@@ -200,7 +200,7 @@ namespace Content.Client.Preferences.UI
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
|
||||
UpdateCMarkingsHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_facialHairPicker.OnMarkingSelect += newStyle =>
|
||||
@@ -209,7 +209,7 @@ namespace Content.Client.Preferences.UI
|
||||
return;
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_facialHairPicker.OnColorChanged += newColor =>
|
||||
@@ -219,7 +219,7 @@ namespace Content.Client.Preferences.UI
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
|
||||
UpdateCMarkingsFacialHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_hairPicker.OnSlotRemove += _ =>
|
||||
@@ -231,7 +231,7 @@ namespace Content.Client.Preferences.UI
|
||||
);
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_facialHairPicker.OnSlotRemove += _ =>
|
||||
@@ -243,7 +243,7 @@ namespace Content.Client.Preferences.UI
|
||||
);
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsFacialHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_hairPicker.OnSlotAdd += delegate()
|
||||
@@ -263,7 +263,7 @@ namespace Content.Client.Preferences.UI
|
||||
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_facialHairPicker.OnSlotAdd += delegate()
|
||||
@@ -283,7 +283,7 @@ namespace Content.Client.Preferences.UI
|
||||
|
||||
UpdateHairPickers();
|
||||
UpdateCMarkingsFacialHair();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
#endregion Hair
|
||||
@@ -312,7 +312,7 @@ namespace Content.Client.Preferences.UI
|
||||
Profile = Profile.WithCharacterAppearance(
|
||||
Profile.Appearance.WithEyeColor(newColor));
|
||||
CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
#endregion Eyes
|
||||
@@ -336,7 +336,7 @@ namespace Content.Client.Preferences.UI
|
||||
_preferenceUnavailableButton.SelectId(args.Id);
|
||||
|
||||
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
_jobPriorities = new List<JobPrioritySelector>();
|
||||
@@ -369,7 +369,7 @@ namespace Content.Client.Preferences.UI
|
||||
selector.PreferenceChanged += preference =>
|
||||
{
|
||||
Profile = Profile?.WithTraitPreference(trait.ID, preference);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -436,6 +436,13 @@ namespace Content.Client.Preferences.UI
|
||||
LoadServerData();
|
||||
}
|
||||
|
||||
ShowClothes.OnToggled += args =>
|
||||
{
|
||||
var lobby = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
lobby.SetClothes(args.Pressed);
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
preferencesManager.OnServerDataLoaded += LoadServerData;
|
||||
|
||||
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
||||
@@ -443,6 +450,15 @@ namespace Content.Client.Preferences.UI
|
||||
UpdateSpeciesGuidebookIcon();
|
||||
|
||||
IsDirty = false;
|
||||
controller.UpdateProfile();
|
||||
}
|
||||
|
||||
private void SetDirty()
|
||||
{
|
||||
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.UpdateProfile(Profile);
|
||||
controller.ReloadCharacterUI();
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
@@ -487,13 +503,13 @@ namespace Content.Client.Preferences.UI
|
||||
if (selector.Disabled)
|
||||
{
|
||||
Profile = Profile?.WithAntagPreference(antag.ID, false);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
selector.PreferenceChanged += preference =>
|
||||
{
|
||||
Profile = Profile?.WithAntagPreference(antag.ID, preference);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -562,7 +578,10 @@ namespace Content.Client.Preferences.UI
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
RoleLoadout? loadout = null;
|
||||
|
||||
// Clone so we don't modify the underlying loadout.
|
||||
Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
|
||||
loadout = loadout?.Clone();
|
||||
var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
|
||||
{
|
||||
Margin = new Thickness(3f, 3f, 3f, 0f),
|
||||
@@ -578,15 +597,13 @@ namespace Content.Client.Preferences.UI
|
||||
|
||||
selector.LoadoutUpdated += args =>
|
||||
{
|
||||
Profile?.SetLoadout(args);
|
||||
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
|
||||
IsDirty = true;
|
||||
Profile = Profile?.WithLoadout(args);
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
selector.PriorityChanged += priority =>
|
||||
{
|
||||
Profile = Profile?.WithJobPriority(job.ID, priority);
|
||||
IsDirty = true;
|
||||
|
||||
foreach (var jobSelector in _jobPriorities)
|
||||
{
|
||||
@@ -602,6 +619,8 @@ namespace Content.Client.Preferences.UI
|
||||
Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
|
||||
}
|
||||
}
|
||||
|
||||
SetDirty();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -619,7 +638,7 @@ namespace Content.Client.Preferences.UI
|
||||
return;
|
||||
|
||||
Profile = Profile.WithFlavorText(content);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void OnMarkingChange(MarkingSet markings)
|
||||
@@ -628,8 +647,10 @@ namespace Content.Client.Preferences.UI
|
||||
return;
|
||||
|
||||
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
|
||||
UpdatePreview();
|
||||
IsDirty = true;
|
||||
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.UpdateProfile(Profile);
|
||||
controller.ReloadProfile();
|
||||
}
|
||||
|
||||
private void OnSkinColorOnValueChanged()
|
||||
@@ -683,6 +704,9 @@ namespace Content.Client.Preferences.UI
|
||||
}
|
||||
|
||||
IsDirty = true;
|
||||
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.UpdateProfile(Profile);
|
||||
controller.ReloadProfile();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -698,7 +722,7 @@ namespace Content.Client.Preferences.UI
|
||||
_preferencesManager.OnServerDataLoaded -= LoadServerData;
|
||||
}
|
||||
|
||||
private void LoadServerData()
|
||||
public void LoadServerData()
|
||||
{
|
||||
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
|
||||
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
|
||||
@@ -706,12 +730,13 @@ namespace Content.Client.Preferences.UI
|
||||
UpdateAntagRequirements();
|
||||
UpdateRoleRequirements();
|
||||
UpdateControls();
|
||||
ShowClothes.Pressed = true;
|
||||
}
|
||||
|
||||
private void SetAge(int newAge)
|
||||
{
|
||||
Profile = Profile?.WithAge(newAge);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void SetSex(Sex newSex)
|
||||
@@ -732,13 +757,13 @@ namespace Content.Client.Preferences.UI
|
||||
}
|
||||
UpdateGenderControls();
|
||||
CMarkings.SetSex(newSex);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void SetGender(Gender newGender)
|
||||
{
|
||||
Profile = Profile?.WithGender(newGender);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void SetSpecies(string newSpecies)
|
||||
@@ -748,20 +773,20 @@ namespace Content.Client.Preferences.UI
|
||||
CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
|
||||
UpdateSexControls(); // update sex for new species
|
||||
UpdateSpeciesGuidebookIcon();
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
private void SetName(string newName)
|
||||
{
|
||||
Profile = Profile?.WithName(newName);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
||||
{
|
||||
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
|
||||
IsDirty = true;
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
@@ -773,6 +798,8 @@ namespace Content.Client.Preferences.UI
|
||||
|
||||
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
|
||||
OnProfileChanged?.Invoke(Profile, CharacterSlot);
|
||||
// Reset profile to default.
|
||||
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateProfile();
|
||||
}
|
||||
|
||||
private bool IsDirty
|
||||
@@ -1065,7 +1092,7 @@ namespace Content.Client.Preferences.UI
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
|
||||
UserInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
|
||||
SetPreviewRotation(_previewRotation);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototyp
|
||||
_loadout.EnsureValid(session, collection);
|
||||
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
|
||||
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.UpdateCharacterUI();
|
||||
controller.ReloadProfile();
|
||||
LoadoutUpdated?.Invoke(_loadout);
|
||||
};
|
||||
|
||||
@@ -165,7 +165,7 @@ public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototyp
|
||||
_loadout.EnsureValid(session, collection);
|
||||
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
|
||||
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
||||
controller.UpdateCharacterUI();
|
||||
controller.ReloadProfile();
|
||||
LoadoutUpdated?.Invoke(_loadout);
|
||||
};
|
||||
|
||||
|
||||
@@ -315,6 +315,7 @@ namespace Content.Shared.Preferences
|
||||
list.Remove(antagId);
|
||||
}
|
||||
}
|
||||
|
||||
return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
|
||||
}
|
||||
|
||||
@@ -565,6 +566,23 @@ namespace Content.Shared.Preferences
|
||||
_loadouts[loadout.Role.Id] = loadout;
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithLoadout(RoleLoadout loadout)
|
||||
{
|
||||
// Deep copies so we don't modify the DB profile.
|
||||
var copied = new Dictionary<string, RoleLoadout>();
|
||||
|
||||
foreach (var proto in _loadouts)
|
||||
{
|
||||
if (proto.Key == loadout.Role)
|
||||
continue;
|
||||
|
||||
copied[proto.Key] = proto.Value.Clone();
|
||||
}
|
||||
|
||||
copied[loadout.Role] = loadout.Clone();
|
||||
return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied);
|
||||
}
|
||||
|
||||
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
|
||||
{
|
||||
if (!_loadouts.TryGetValue(id, out var loadout))
|
||||
|
||||
@@ -29,6 +29,18 @@ public sealed class RoleLoadout
|
||||
Role = role;
|
||||
}
|
||||
|
||||
public RoleLoadout Clone()
|
||||
{
|
||||
var weh = new RoleLoadout(Role);
|
||||
|
||||
foreach (var selected in SelectedLoadouts)
|
||||
{
|
||||
weh.SelectedLoadouts.Add(selected.Key, new List<Loadout>(selected.Value));
|
||||
}
|
||||
|
||||
return weh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all prototypes exist and effects can be applied.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user