Merge pull request 'Changes of admin ui for banning users.' (#128) from ban-admin-changes into 2.0.3

Reviewed-on: #128
This commit was merged in pull request #128.
This commit is contained in:
2026-01-04 14:49:47 +00:00
2 changed files with 386 additions and 63 deletions

View File

@@ -8,6 +8,7 @@ using LightlessSync.UI.Tags;
using LightlessSync.WebAPI;
using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging;
using LightlessSync.PlayerData.Factories;
namespace LightlessSync.Services;
@@ -23,6 +24,7 @@ public class UiFactory
private readonly PerformanceCollectorService _performanceCollectorService;
private readonly ProfileTagService _profileTagService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly PairFactory _pairFactory;
public UiFactory(
ILoggerFactory loggerFactory,
@@ -34,7 +36,8 @@ public class UiFactory
LightlessProfileManager lightlessProfileManager,
PerformanceCollectorService performanceCollectorService,
ProfileTagService profileTagService,
DalamudUtilService dalamudUtilService)
DalamudUtilService dalamudUtilService,
PairFactory pairFactory)
{
_loggerFactory = loggerFactory;
_lightlessMediator = lightlessMediator;
@@ -46,6 +49,7 @@ public class UiFactory
_performanceCollectorService = performanceCollectorService;
_profileTagService = profileTagService;
_dalamudUtilService = dalamudUtilService;
_pairFactory = pairFactory;
}
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
@@ -58,7 +62,8 @@ public class UiFactory
_pairUiService,
dto,
_performanceCollectorService,
_lightlessProfileManager);
_lightlessProfileManager,
_pairFactory);
}
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)

View File

@@ -4,9 +4,11 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto.Group;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
@@ -42,13 +44,32 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
private Task<int>? _pruneTask;
private int _pruneDays = 14;
// Ban management fields
private Task<List<BannedGroupUserDto>>? _bannedUsersTask;
private bool _bannedUsersLoaded;
private string? _bannedUsersLoadError;
private string _newBanUid = string.Empty;
private string _newBanReason = string.Empty;
private Task? _newBanTask;
private string? _newBanError;
private DateTime _newBanBusyUntilUtc;
// Ban editing fields
private string? _editingBanUid;
private readonly Dictionary<string, string> _banReasonEdits = new(StringComparer.Ordinal);
private Task? _banEditTask;
private string? _banEditError;
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
private bool _pruneSettingsLoaded;
private bool _autoPruneEnabled;
private int _autoPruneDays = 14;
private readonly PairFactory _pairFactory;
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager)
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, PairFactory pairFactory)
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
{
GroupFullInfo = groupFullInfo;
@@ -76,6 +97,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
MaximumSize = new(700, 2000),
};
_pairUiService = pairUiService;
_pairFactory = pairFactory;
}
public GroupFullInfoDto GroupFullInfo { get; private set; }
@@ -654,34 +676,345 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(3f);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
EnsureBanListLoaded();
DrawNewBanEntryRow();
ImGuiHelpers.ScaledDummy(4f);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist"))
{
_bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result;
QueueBanListRefresh(force: true);
}
ImGuiHelpers.ScaledDummy(2f);
if (!_bannedUsersLoaded)
{
UiSharedService.ColorTextWrapped("Loading banlist from server...", ImGuiColors.DalamudGrey);
return;
}
if (!string.IsNullOrWhiteSpace(_bannedUsersLoadError))
{
UiSharedService.ColorTextWrapped(_bannedUsersLoadError!, ImGuiColors.DalamudRed);
return;
}
ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true);
var style = ImGui.GetStyle();
float fullW = ImGui.GetContentRegionAvail().X;
float scale = ImGuiHelpers.GlobalScale;
float frame = ImGui.GetFrameHeight();
float actionIcons = 3;
float colActions = actionIcons * frame + (actionIcons - 1) * style.ItemSpacing.X + 10f * scale;
float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f;
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
// Header
float colIdentity = fullW - colMeta - colActions - style.ItemSpacing.X * 2.0f;
float minIdentity = fullW * 0.40f;
if (colIdentity < minIdentity)
{
colIdentity = minIdentity;
colMeta = fullW - colIdentity - colActions - style.ItemSpacing.X * 2.0f;
if (colMeta < 80f * scale) colMeta = 80f * scale;
}
DrawBannedListHeader(colIdentity, colMeta);
int rowIndex = 0;
foreach (var bannedUser in _bannedUsers.ToList())
{
// Each row
DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions);
}
ImGui.EndChild();
}
private void DrawNewBanEntryRow()
{
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
ImGui.TextUnformatted("Add new ban");
ImGui.PopStyleColor();
UiSharedService.TextWrapped("Enter a UID (Not Alias!) and optional reason. (Hold CTRL to enable the ban button.)");
var style = ImGui.GetStyle();
float fullW = ImGui.GetContentRegionAvail().X;
float uidW = fullW * 0.35f;
float reasonW = fullW * 0.50f;
float btnW = fullW - uidW - reasonW - style.ItemSpacing.X * 2f;
// UID
ImGui.SetNextItemWidth(uidW);
ImGui.InputTextWithHint("##newBanUid", "UID...", ref _newBanUid, 128);
// Reason
ImGui.SameLine(0f, style.ItemSpacing.X);
ImGui.SetNextItemWidth(reasonW);
ImGui.InputTextWithHint("##newBanReason", "Reason (optional)...", ref _newBanReason, 256);
// Ban button
ImGui.SameLine(0f, style.ItemSpacing.X);
var trimmedUid = (_newBanUid ?? string.Empty).Trim();
var now = DateTime.UtcNow;
bool taskRunning = _newBanTask != null && !_newBanTask.IsCompleted;
bool busyLatched = now < _newBanBusyUntilUtc;
bool busy = taskRunning || busyLatched;
bool canBan = UiSharedService.CtrlPressed()
&& !string.IsNullOrWhiteSpace(_newBanUid)
&& !busy;
using (ImRaii.Disabled(!canBan))
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
{
ImGui.SetNextItemWidth(btnW);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban"))
{
_newBanError = null;
_newBanBusyUntilUtc = DateTime.UtcNow.AddMilliseconds(750);
_newBanTask = SubmitNewBanByUidAsync(trimmedUid, _newBanReason);
}
}
UiSharedService.AttachToolTip("Hold CTRL to enable banning by UID.");
if (busy)
{
UiSharedService.ColorTextWrapped("Banning user...", ImGuiColors.DalamudGrey);
}
if (_newBanTask != null && _newBanTask.IsCompleted && DateTime.UtcNow >= _newBanBusyUntilUtc)
{
if (_newBanTask.IsFaulted)
{
var _ = _newBanTask.Exception;
_newBanError ??= "Ban failed (see log).";
}
QueueBanListRefresh(force: true);
_newBanTask = null;
}
}
private async Task SubmitNewBanByUidAsync(string uidOrAlias, string reason)
{
try
{
await Task.Yield();
uidOrAlias = (uidOrAlias ?? string.Empty).Trim();
reason = (reason ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(uidOrAlias))
{
_newBanError = "UID is empty.";
return;
}
string targetUid = uidOrAlias;
string? typedAlias = null;
var snap = _pairUiService.GetSnapshot();
if (snap.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
{
var match = pairs.FirstOrDefault(p =>
string.Equals(p.UserData.UID, uidOrAlias, StringComparison.Ordinal) ||
string.Equals(p.UserData.AliasOrUID, uidOrAlias, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
targetUid = match.UserData.UID;
typedAlias = match.UserData.Alias;
}
else
{
typedAlias = null;
}
}
var userData = new UserData(UID: targetUid, Alias: typedAlias);
await _apiController
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), reason)
.ConfigureAwait(false);
_newBanUid = string.Empty;
_newBanReason = string.Empty;
_newBanError = null;
QueueBanListRefresh(force: true);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to ban '{uidOrAlias}' in group {gid}", uidOrAlias, GroupFullInfo.Group.GID);
_newBanError = "Failed to ban user (see log).";
}
}
private async Task SaveBanReasonViaBanUserAsync(string uid)
{
try
{
if (!_banReasonEdits.TryGetValue(uid, out var newReason))
newReason = string.Empty;
newReason = (newReason ?? string.Empty).Trim();
var userData = new UserData(uid.Trim());
await _apiController
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), newReason)
.ConfigureAwait(false);
_editingBanUid = null;
_banEditError = null;
await Task.Delay(450).ConfigureAwait(false);
QueueBanListRefresh(force: true);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to edit ban reason for {uid} in group {gid}", uid, GroupFullInfo.Group.GID);
_banEditError = "Failed to update reason (see log).";
}
}
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
{
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
var style = ImGui.GetStyle();
float x0 = ImGui.GetCursorPosX();
if (rowIndex % 2 == 0)
{
var drawList = ImGui.GetWindowDrawList();
var pMin = ImGui.GetCursorScreenPos();
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
var pMax = new Vector2(
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
pMin.Y + rowHeight);
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
}
ImGui.SetCursorPosX(x0);
ImGui.AlignTextToFramePadding();
string alias = bannedUser.UserAlias ?? string.Empty;
string line1 = string.IsNullOrEmpty(alias)
? bannedUser.UID
: $"{alias} ({bannedUser.UID})";
ImGui.TextUnformatted(line1);
var fullReason = bannedUser.Reason ?? string.Empty;
if (string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal))
{
_banReasonEdits.TryGetValue(bannedUser.UID, out var editReason);
editReason ??= StripAliasSuffix(fullReason);
ImGui.SetCursorPosX(x0);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.SetNextItemWidth(colIdentity);
ImGui.InputTextWithHint("##banReasonEdit", "Reason...", ref editReason, 255);
ImGui.PopStyleColor();
_banReasonEdits[bannedUser.UID] = editReason;
if (!string.IsNullOrWhiteSpace(_banEditError))
UiSharedService.ColorTextWrapped(_banEditError!, ImGuiColors.DalamudRed);
}
else
{
if (!string.IsNullOrWhiteSpace(fullReason))
{
ImGui.SetCursorPosX(x0);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.PushTextWrapPos(x0 + colIdentity);
UiSharedService.TextWrapped(fullReason);
ImGui.PopTextWrapPos();
ImGui.PopStyleColor();
}
}
ImGui.SameLine();
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextUnformatted(dateText);
ImGui.PopStyleColor();
ImGui.SameLine();
float frame = ImGui.GetFrameHeight();
float actionsX0 = x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f;
ImGui.SameLine();
ImGui.SetCursorPosX(actionsX0);
bool isEditing = string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal);
int actionCount = 1 + (isEditing ? 2 : 1);
float totalW = actionCount * frame + (actionCount - 1) * style.ItemSpacing.X;
float startX = actionsX0 + MathF.Max(0, colActions - totalW) - 36f;
ImGui.SetCursorPosX(startX);
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
{
_apiController.GroupUnbanUser(bannedUser);
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
}
UiSharedService.AttachToolTip("Unban");
ImGui.SameLine(0f, style.ItemSpacing.X);
if (!isEditing)
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Edit))
{
_banEditError = null;
_editingBanUid = bannedUser.UID;
_banReasonEdits[bannedUser.UID] = StripAliasSuffix(bannedUser.Reason ?? string.Empty);
}
UiSharedService.AttachToolTip("Edit reason");
}
else
{
if (_uiSharedService.IconButton(FontAwesomeIcon.Save))
{
_banEditError = null;
_banEditTask = SaveBanReasonViaBanUserAsync(bannedUser.UID);
}
UiSharedService.AttachToolTip("Save");
ImGui.SameLine(0f, style.ItemSpacing.X);
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
{
_banEditError = null;
_editingBanUid = null;
}
UiSharedService.AttachToolTip("Cancel");
}
}
private void DrawInvites(GroupPermissions perm)
{
var inviteTab = ImRaii.TabItem("Invites");
@@ -902,7 +1235,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
if (buttonCount == 0)
return;
float totalWidth = buttonCount * frameH + (buttonCount - 1) * style.ItemSpacing.X;
float totalWidth = _isOwner
? buttonCount * frameH + buttonCount * style.ItemSpacing.X + 20f
: buttonCount * frameH + buttonCount * style.ItemSpacing.X;
float curX = ImGui.GetCursorPosX();
float avail = ImGui.GetContentRegionAvail().X;
@@ -1031,69 +1366,40 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f);
}
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
private void QueueBanListRefresh(bool force = false)
{
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
var style = ImGui.GetStyle();
float x0 = ImGui.GetCursorPosX();
if (rowIndex % 2 == 0)
if (!force)
{
var drawList = ImGui.GetWindowDrawList();
var pMin = ImGui.GetCursorScreenPos();
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
var pMax = new Vector2(
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
pMin.Y + rowHeight);
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
if (_bannedUsersTask != null && !_bannedUsersTask.IsCompleted)
return;
}
ImGui.SetCursorPosX(x0);
ImGui.AlignTextToFramePadding();
_bannedUsersLoaded = false;
_bannedUsersLoadError = null;
string alias = bannedUser.UserAlias ?? string.Empty;
string line1 = string.IsNullOrEmpty(alias)
? bannedUser.UID
: $"{alias} ({bannedUser.UID})";
_bannedUsersTask = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
}
ImGui.TextUnformatted(line1);
private void EnsureBanListLoaded()
{
_bannedUsersTask ??= _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
var reason = bannedUser.Reason ?? string.Empty;
if (!string.IsNullOrWhiteSpace(reason))
if (_bannedUsersLoaded || _bannedUsersTask == null)
return;
if (!_bannedUsersTask.IsCompleted)
return;
if (_bannedUsersTask.IsFaulted || _bannedUsersTask.IsCanceled)
{
var reasonPos = new Vector2(x0, ImGui.GetCursorPosY());
ImGui.SetCursorPos(reasonPos);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
UiSharedService.TextWrapped(reason);
ImGui.PopStyleColor();
_bannedUsersLoadError = "Failed to load banlist from server.";
_bannedUsers = [];
_bannedUsersLoaded = true;
return;
}
ImGui.SameLine();
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextUnformatted(dateText);
ImGui.PopStyleColor();
ImGui.SameLine();
ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban"))
{
_apiController.GroupUnbanUser(bannedUser);
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
}
UiSharedService.AttachToolTip($"Unban {alias} ({bannedUser.UID}) from this Syncshell");
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
_bannedUsers = _bannedUsersTask.GetAwaiter().GetResult() ?? [];
_bannedUsersLoaded = true;
}
private void SavePruneSettings()
@@ -1116,6 +1422,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
}
}
private static string StripAliasSuffix(string reason)
{
const string marker = " (Alias at time of ban:";
var idx = reason.IndexOf(marker, StringComparison.Ordinal);
return idx >= 0 ? reason[..idx] : reason;
}
private static bool MatchesUserFilter(Pair pair, string filterLower)
{
var note = pair.GetNote() ?? string.Empty;
@@ -1127,6 +1440,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|| alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase);
}
public override void OnOpen()
{
base.OnOpen();
QueueBanListRefresh(force: true);
}
public override void OnClose()
{
Mediator.Publish(new RemoveWindowMessage(this));