Update: Mentor (#3478)
This commit is contained in:
parent
fab5bc3f9b
commit
acec175fa3
14 changed files with 882 additions and 280 deletions
|
|
@ -6,9 +6,12 @@
|
|||
<BoxContainer Orientation="Horizontal" SetHeight="40" HorizontalExpand="True" Margin="5">
|
||||
<Button Name="NewTicketButton" Access="Public" Text="{Loc 'mentor-help-new-ticket'}"
|
||||
StyleClasses="ButtonBig" Margin="0 0 10 0"/>
|
||||
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}" Margin="0 0 10 0" />
|
||||
<Button Name="AdminWhoButton" Access="Public" Text="{Loc 'admin-who-button'}"
|
||||
StyleClasses="ButtonBig" Margin="0 0 10 0"/>
|
||||
<Button Name="StatisticsButton" Access="Public" Text="{Loc 'mentor-help-statistics'}"
|
||||
StyleClasses="ButtonBig" Visible="False" Margin="0 0 10 0" />
|
||||
<CheckBox Name="AutoOpenTickets" Access="Public" Text="{Loc 'mentor-help-auto-open-tickets'}" ToolTip="{Loc 'mentor-help-auto-open-tickets-tooltip'}" HorizontalAlignment="Right" Margin="0 0 10 0"/>
|
||||
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'help-kwoink-play-sound'}" Pressed="True" HorizontalAlignment="Right"/>
|
||||
<Control HorizontalExpand="True" />
|
||||
<Button Name="BackToListButton" Access="Public" Text="{Loc 'mentor-help-back-to-list'}"
|
||||
StyleClasses="ButtonBig" HorizontalAlignment="Right" Visible="False" />
|
||||
|
|
@ -18,7 +21,6 @@
|
|||
<TabContainer Name="TicketsTabContainer" Access="Public" VerticalExpand="True" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-id'}" SizeFlagsStretchRatio="1" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-player'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
|
|
@ -28,7 +30,6 @@
|
|||
<Label Text="{Loc 'mentor-help-column-assigned'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-subject'}" SizeFlagsStretchRatio="5" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
|
|
@ -39,7 +40,6 @@
|
|||
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-id'}" SizeFlagsStretchRatio="1" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-player'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
|
|
@ -49,7 +49,6 @@
|
|||
<Label Text="{Loc 'mentor-help-column-assigned'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-column-subject'}" SizeFlagsStretchRatio="5" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
|
|
@ -79,11 +78,12 @@
|
|||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<ScrollContainer Name="MessagesScroll" Access="Public" VerticalExpand="True" HorizontalExpand="True" Margin="5">
|
||||
<ScrollContainer Name="MessagesScroll" Access="Public" VerticalExpand="True" HorizontalExpand="True" Margin="5" HScrollEnabled="False">
|
||||
<BoxContainer Name="MessagesContainer" Access="Public" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
|
||||
<BoxContainer Name="ReplyPanel" Access="Public" Orientation="Vertical" HorizontalExpand="True" Margin="5">
|
||||
<RichTextLabel Name="TypingIndicator" Access="Public" />
|
||||
<LineEdit Name="ReplyInput" Access="Public" PlaceHolder="{Loc 'mentor-help-reply-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ using Robust.Client.UserInterface.Controls;
|
|||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Configuration;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
|
|
@ -21,7 +26,13 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
public sealed partial class MentorHelpControl : Control
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private const double CloseConfirmTimeoutSeconds = 2;
|
||||
private const double TypingIndicatorTimeoutSeconds = 10;
|
||||
private bool _isDisposed;
|
||||
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
private NetUserId _ownerUserId;
|
||||
|
|
@ -29,6 +40,7 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
private List<MentorHelpTicketData> _tickets = new();
|
||||
private MentorHelpTicketData? _selectedTicket;
|
||||
private Dictionary<int, List<MentorHelpMessageData>> _ticketMessages = new();
|
||||
private readonly HashSet<int> _newMessageFromAuthorTickets = new();
|
||||
|
||||
private readonly Dictionary<int, TicketEntryControl> _openTicketControls = new();
|
||||
private readonly Dictionary<int, TicketEntryControl> _closedTicketControls = new();
|
||||
|
|
@ -41,12 +53,25 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
private MentorHelpNewTicketDialog? _newTicketDialog;
|
||||
private int? _pendingOpenTicketId;
|
||||
private List<string> PeopleTyping { get; set; } = new();
|
||||
private readonly Dictionary<string, TimeSpan> _typingTimeouts = new();
|
||||
public event Action<string>? InputTextChanged;
|
||||
|
||||
private bool _closeConfirming;
|
||||
private int? _closeConfirmTicketId;
|
||||
private TimeSpan? _closeConfirmResetOn;
|
||||
|
||||
public MentorHelpControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_cfg.OnValueChanged(SunriseCCVars.MentorHelpSoundEnabled, OnMentorHelpSoundEnabledChanged, true);
|
||||
_cfg.OnValueChanged(SunriseCCVars.MentorHelpAutoOpenOnNewMessage, OnMentorHelpAutoOpenChanged, true);
|
||||
|
||||
PlaySound.OnToggled += OnPlaySoundToggled;
|
||||
AutoOpenTickets.OnToggled += OnAutoOpenTicketsToggled;
|
||||
|
||||
// Wire up button events
|
||||
NewTicketButton.OnPressed += _ => OpenNewTicketDialog();
|
||||
StatisticsButton.OnPressed += _ => _ui.GetUIController<MentorHelpStatisticsUIController>().ToggleStatistics();
|
||||
|
|
@ -59,10 +84,12 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
// Handle enter key in reply input
|
||||
ReplyInput.OnTextEntered += _ => SendReply();
|
||||
ReplyInput.OnTextChanged += Input_OnTextChanged;
|
||||
UpdateTypingIndicator();
|
||||
|
||||
// Setup tab container like in AdminMenuWindow
|
||||
TicketsTabContainer.SetTabTitle(0, Loc.GetString("mentor-help-tab-open"));
|
||||
TicketsTabContainer.SetTabTitle(1, Loc.GetString("mentor-help-tab-closed"));
|
||||
TicketsTabContainer.SetTabTitle(0, _loc.GetString("mentor-help-tab-open"));
|
||||
TicketsTabContainer.SetTabTitle(1, _loc.GetString("mentor-help-tab-closed"));
|
||||
|
||||
// Handle tab changes to load closed tickets when needed
|
||||
TicketsTabContainer.OnTabChanged += OnTabChanged;
|
||||
|
|
@ -75,6 +102,37 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
// Set initial state
|
||||
SwitchState(ViewState.TicketsList);
|
||||
|
||||
MessagesContainer.OnChildAdded += _ =>
|
||||
{
|
||||
MessagesScroll.VScrollTarget = float.MaxValue;
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPlaySoundToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_cfg.SetCVar(SunriseCCVars.MentorHelpSoundEnabled, args.Pressed);
|
||||
}
|
||||
|
||||
private void OnAutoOpenTicketsToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_cfg.SetCVar(SunriseCCVars.MentorHelpAutoOpenOnNewMessage, args.Pressed);
|
||||
}
|
||||
|
||||
private void OnMentorHelpSoundEnabledChanged(bool enabled)
|
||||
{
|
||||
PlaySound.Pressed = enabled;
|
||||
}
|
||||
|
||||
private void OnMentorHelpAutoOpenChanged(bool enabled)
|
||||
{
|
||||
AutoOpenTickets.Pressed = enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -123,6 +181,15 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
// State switching like in LobbyGui
|
||||
private void SwitchState(ViewState state)
|
||||
{
|
||||
if (state == ViewState.TicketsList && _selectedTicket != null)
|
||||
{
|
||||
_mentorHelpSystem?.SendInputTextUpdated(_selectedTicket.Id, false);
|
||||
PeopleTyping.Clear();
|
||||
_typingTimeouts.Clear();
|
||||
UpdateTypingIndicator();
|
||||
ResetCloseConfirm();
|
||||
}
|
||||
|
||||
DefaultState.Visible = false;
|
||||
TicketViewState.Visible = false;
|
||||
BackToListButton.Visible = false;
|
||||
|
|
@ -201,11 +268,13 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
if (controls.TryGetValue(ticket.Id, out var existingControl))
|
||||
{
|
||||
existingControl.UpdateData(ticket);
|
||||
existingControl.SetNewMessageFromAuthor(_newMessageFromAuthorTickets.Contains(ticket.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
var control = new TicketEntryControl();
|
||||
control.UpdateData(ticket);
|
||||
control.SetNewMessageFromAuthor(_newMessageFromAuthorTickets.Contains(ticket.Id));
|
||||
control.OnTicketSelected += OnTicketSelected;
|
||||
controls[ticket.Id] = control;
|
||||
container.AddChild(control);
|
||||
|
|
@ -215,7 +284,19 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
private void OnTicketSelected(MentorHelpTicketData ticket)
|
||||
{
|
||||
if (_selectedTicket != null)
|
||||
_mentorHelpSystem?.SendInputTextUpdated(_selectedTicket.Id, false);
|
||||
|
||||
_selectedTicket = ticket;
|
||||
PeopleTyping.Clear();
|
||||
_typingTimeouts.Clear();
|
||||
UpdateTypingIndicator();
|
||||
ReplyInput.Text = string.Empty;
|
||||
ResetCloseConfirm();
|
||||
|
||||
_newMessageFromAuthorTickets.Remove(ticket.Id);
|
||||
ticket.HasUnreadMessages = false;
|
||||
UpdateTicket(ticket);
|
||||
SwitchState(ViewState.TicketView);
|
||||
|
||||
// Clear previous messages first
|
||||
|
|
@ -229,9 +310,45 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
{
|
||||
_ticketMessages[ticketId] = messages;
|
||||
|
||||
var ticketIndex = _tickets.FindIndex(t => t.Id == ticketId);
|
||||
if (ticketIndex >= 0 && messages.Count > 0)
|
||||
{
|
||||
var ticket = _tickets[ticketIndex];
|
||||
var lastMessage = messages[^1];
|
||||
|
||||
if (ticket.UpdatedAt < lastMessage.SentAt)
|
||||
ticket.UpdatedAt = lastMessage.SentAt;
|
||||
|
||||
var isViewingTicket = _selectedTicket?.Id == ticketId && TicketViewState.Visible;
|
||||
|
||||
var isRelevant = !_hasMentorPermissions
|
||||
? ticket.PlayerId == _ownerUserId
|
||||
: ticket.AssignedToUserId == null || ticket.AssignedToUserId == _ownerUserId;
|
||||
|
||||
if (isRelevant)
|
||||
{
|
||||
var unread = !isViewingTicket && lastMessage.SenderUserId != _ownerUserId;
|
||||
ticket.HasUnreadMessages = unread;
|
||||
|
||||
if (_hasMentorPermissions && unread && lastMessage.SenderUserId == ticket.PlayerId)
|
||||
_newMessageFromAuthorTickets.Add(ticketId);
|
||||
else
|
||||
_newMessageFromAuthorTickets.Remove(ticketId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ticket.HasUnreadMessages = false;
|
||||
_newMessageFromAuthorTickets.Remove(ticketId);
|
||||
}
|
||||
|
||||
_tickets[ticketIndex] = ticket;
|
||||
RefreshTicketsList();
|
||||
}
|
||||
|
||||
if (_selectedTicket?.Id == ticketId && TicketViewState.Visible)
|
||||
{
|
||||
DisplayTicketMessages(messages);
|
||||
ScrollToBottomDeferred();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,10 +399,10 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
TicketSubjectLabel.Text = _selectedTicket.Subject;
|
||||
|
||||
// Компактная строка: статус | назначен | создано
|
||||
TicketStatus.Text = Loc.GetString("mentor-help-status-label", ("status", GetStatusText(_selectedTicket.Status)));
|
||||
TicketAssigned.Text = Loc.GetString("mentor-help-assigned-label",
|
||||
("assigned", _selectedTicket.AssignedToName ?? Loc.GetString("mentor-help-unassigned")));
|
||||
TicketCreated.Text = Loc.GetString("mentor-help-created-label",
|
||||
TicketStatus.Text = _loc.GetString("mentor-help-status-label", ("status", GetStatusText(_selectedTicket.Status)));
|
||||
TicketAssigned.Text = _loc.GetString("mentor-help-assigned-label",
|
||||
("assigned", _selectedTicket.AssignedToName ?? _loc.GetString("mentor-help-unassigned")));
|
||||
TicketCreated.Text = _loc.GetString("mentor-help-created-label",
|
||||
("created", _selectedTicket.CreatedAt.ToString("dd.MM.yyyy HH:mm")));
|
||||
}
|
||||
|
||||
|
|
@ -300,13 +417,17 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
var canReply = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||||
var isAssignedToMe = _selectedTicket.AssignedToUserId == _ownerUserId;
|
||||
var isOpen = _selectedTicket.Status != MentorHelpTicketStatus.Closed;
|
||||
var isUnassigned = _selectedTicket.AssignedToUserId == null;
|
||||
|
||||
// Hide reply panel for closed tickets
|
||||
ReplyPanel.Visible = canReply;
|
||||
|
||||
if (!isOpen)
|
||||
ResetCloseConfirm();
|
||||
|
||||
if (_hasMentorPermissions)
|
||||
{
|
||||
ClaimButton.Visible = isOpen && !isAssignedToMe;
|
||||
ClaimButton.Visible = isOpen && isUnassigned;
|
||||
UnassignButton.Visible = isOpen && isAssignedToMe;
|
||||
CloseTicketButton.Visible = isOpen;
|
||||
}
|
||||
|
|
@ -318,67 +439,83 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
}
|
||||
}
|
||||
|
||||
private void ResetCloseConfirm()
|
||||
{
|
||||
_closeConfirming = false;
|
||||
_closeConfirmTicketId = null;
|
||||
_closeConfirmResetOn = null;
|
||||
CloseTicketButton.Text = _loc.GetString("mentor-help-close-ticket");
|
||||
CloseTicketButton.StyleClasses.Remove(StyleNano.StyleClassButtonColorRed);
|
||||
}
|
||||
|
||||
private string GetStatusText(MentorHelpTicketStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MentorHelpTicketStatus.Open => Loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => Loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => Loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => Loc.GetString("mentor-help-status-closed"),
|
||||
_ => Loc.GetString("mentor-help-status-unknown")
|
||||
MentorHelpTicketStatus.Open => _loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => _loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => _loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => _loc.GetString("mentor-help-status-closed"),
|
||||
_ => _loc.GetString("mentor-help-status-unknown")
|
||||
};
|
||||
}
|
||||
|
||||
private void DisplayTicketMessages(List<MentorHelpMessageData> messages)
|
||||
{
|
||||
MessagesContainer.RemoveAllChildren();
|
||||
|
||||
DateTime? lastDate = null;
|
||||
|
||||
foreach (var message in messages.OrderBy(m => m.SentAt))
|
||||
{
|
||||
// Ensure we show a date header when the day changes
|
||||
var sentAt = message.SentAt;
|
||||
var sentDate = sentAt.Date;
|
||||
|
||||
if (lastDate == null || lastDate.Value != sentDate)
|
||||
{
|
||||
lastDate = sentDate;
|
||||
|
||||
var dateLabel = new RichTextLabel
|
||||
{
|
||||
Text = $"[center=\"{sentAt:dd.MM.yyyy}\"]",
|
||||
HorizontalExpand = true
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 10, 0, 5)
|
||||
};
|
||||
|
||||
dateLabel.Text = $"[center][bold]{sentAt:dd.MM.yyyy}[/bold][/center]";
|
||||
|
||||
MessagesContainer.AddChild(dateLabel);
|
||||
}
|
||||
|
||||
var messageBox = new PanelContainer
|
||||
{
|
||||
StyleClasses = { "PanelColorMedium" },
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 2)
|
||||
};
|
||||
|
||||
var vbox = new BoxContainer
|
||||
var messageBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
Margin = new Thickness(0, 4)
|
||||
};
|
||||
|
||||
// Format: [HH:mm] Author: message
|
||||
var content = new RichTextLabel
|
||||
var nameLine = new RichTextLabel();
|
||||
nameLine.Text = $"[bold]{sentAt:HH:mm}[/bold] {message.FormattedSender}:";
|
||||
|
||||
var textLine = new RichTextLabel
|
||||
{
|
||||
Text = $"[bold]{sentAt:HH:mm}[/bold] {message.FormattedSender}: {message.Message}",
|
||||
HorizontalExpand = true
|
||||
};
|
||||
textLine.Text = message.Message;
|
||||
|
||||
messageBox.AddChild(nameLine);
|
||||
messageBox.AddChild(textLine);
|
||||
|
||||
vbox.AddChild(content);
|
||||
messageBox.AddChild(vbox);
|
||||
MessagesContainer.AddChild(messageBox);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToBottomDeferred()
|
||||
{
|
||||
_ui.DeferAction(() =>
|
||||
{
|
||||
MessagesScroll.VScrollTarget = float.MaxValue;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void OpenNewTicketDialog()
|
||||
{
|
||||
if (_newTicketDialog != null)
|
||||
|
|
@ -428,7 +565,122 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
if (!_closeConfirming || _closeConfirmTicketId != _selectedTicket.Id)
|
||||
{
|
||||
_closeConfirming = true;
|
||||
_closeConfirmTicketId = _selectedTicket.Id;
|
||||
_closeConfirmResetOn = _gameTiming.RealTime + TimeSpan.FromSeconds(CloseConfirmTimeoutSeconds);
|
||||
CloseTicketButton.Text = _loc.GetString("mentor-help-close-confirm");
|
||||
CloseTicketButton.StyleClasses.Add(StyleNano.StyleClassButtonColorRed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ResetCloseConfirm();
|
||||
_mentorHelpSystem?.CloseTicket(_selectedTicket.Id);
|
||||
}
|
||||
|
||||
private void UpdateTypingIndicator()
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
msg.PushColor(Color.LightGray);
|
||||
|
||||
var text = PeopleTyping.Count == 0
|
||||
? string.Empty
|
||||
: _loc.GetString("bwoink-system-typing-indicator",
|
||||
("players", string.Join(", ", PeopleTyping)),
|
||||
("count", PeopleTyping.Count));
|
||||
|
||||
msg.AddText(text);
|
||||
msg.Pop();
|
||||
|
||||
TypingIndicator.SetMessage(msg);
|
||||
}
|
||||
|
||||
public void UpdatePlayerTyping(int ticketId, string name, bool typing)
|
||||
{
|
||||
if (_selectedTicket?.Id != ticketId || !TicketViewState.Visible)
|
||||
return;
|
||||
|
||||
if (typing)
|
||||
{
|
||||
var now = _gameTiming.RealTime;
|
||||
_typingTimeouts[name] = now + TimeSpan.FromSeconds(TypingIndicatorTimeoutSeconds);
|
||||
|
||||
if (!PeopleTyping.Contains(name))
|
||||
PeopleTyping.Add(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
PeopleTyping.Remove(name);
|
||||
_typingTimeouts.Remove(name);
|
||||
}
|
||||
|
||||
UpdateTypingIndicator();
|
||||
}
|
||||
|
||||
private void Input_OnTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
InputTextChanged?.Invoke(args.Text);
|
||||
|
||||
if (_selectedTicket == null)
|
||||
return;
|
||||
|
||||
_mentorHelpSystem?.SendInputTextUpdated(_selectedTicket.Id, args.Text.Length > 0);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
var now = _gameTiming.RealTime;
|
||||
|
||||
if (_closeConfirmResetOn < now)
|
||||
{
|
||||
if (_closeConfirming && _closeConfirmTicketId == _selectedTicket?.Id)
|
||||
ResetCloseConfirm();
|
||||
|
||||
_closeConfirmResetOn = null;
|
||||
}
|
||||
|
||||
if (PeopleTyping.Count == 0)
|
||||
return;
|
||||
|
||||
var updatedTypingIndicator = false;
|
||||
|
||||
for (var g = PeopleTyping.Count - 1; g >= 0; g--)
|
||||
{
|
||||
var name = PeopleTyping[g];
|
||||
|
||||
if (!_typingTimeouts.TryGetValue(name, out var timeoutAt) || timeoutAt > now)
|
||||
continue;
|
||||
|
||||
PeopleTyping.RemoveAt(g);
|
||||
_typingTimeouts.Remove(name);
|
||||
updatedTypingIndicator = true;
|
||||
}
|
||||
|
||||
if (updatedTypingIndicator)
|
||||
UpdateTypingIndicator();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
PlaySound.OnToggled -= OnPlaySoundToggled;
|
||||
AutoOpenTickets.OnToggled -= OnAutoOpenTicketsToggled;
|
||||
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.MentorHelpSoundEnabled, OnMentorHelpSoundEnabledChanged);
|
||||
_cfg.UnsubValueChanged(SunriseCCVars.MentorHelpAutoOpenOnNewMessage, OnMentorHelpAutoOpenChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
|
|
@ -11,11 +13,14 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MentorHelpNewTicketDialog : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
public event Action<string, string>? OnTicketCreated;
|
||||
|
||||
public MentorHelpNewTicketDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
CancelButton.OnPressed += _ => Close();
|
||||
CreateButton.OnPressed += _ =>
|
||||
|
|
@ -25,14 +30,14 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
if (string.IsNullOrEmpty(subject))
|
||||
{
|
||||
ErrorLabel.Text = Loc.GetString("mentor-help-new-ticket-error-subject");
|
||||
ErrorLabel.Text = _loc.GetString("mentor-help-new-ticket-error-subject");
|
||||
ErrorLabel.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
ErrorLabel.Text = Loc.GetString("mentor-help-new-ticket-error-message");
|
||||
ErrorLabel.Text = _loc.GetString("mentor-help-new-ticket-error-message");
|
||||
ErrorLabel.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,11 @@
|
|||
Title="{Loc 'mentor-help-statistics-title'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="10">
|
||||
<Label Name="HeaderLabel" Access="Public"
|
||||
Text="{Loc 'mentor-help-statistics-header'}"
|
||||
StyleClasses="LabelHeading"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0 15 0">
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Ментор" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Взятых тикетов" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="Сообщений" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center"/>
|
||||
<controls:VSeparator/>
|
||||
<Label Text="{Loc 'mentor-help-statistics-column-mentor'}" SizeFlagsStretchRatio="3" HorizontalExpand="True" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
|
||||
<Label Text="{Loc 'mentor-help-statistics-column-tickets'}" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
|
||||
<Label Text="{Loc 'mentor-help-statistics-column-messages'}" SizeFlagsStretchRatio="2" HorizontalExpand="True" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
|
||||
</BoxContainer>
|
||||
<controls:HSeparator/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Shared._Sunrise.MentorHelp;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
|
|
@ -9,11 +10,17 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
[UsedImplicitly]
|
||||
public sealed class MentorHelpSystem : SharedMentorHelpSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
public event EventHandler<MentorHelpTicketUpdateMessage>? OnTicketUpdated;
|
||||
public event EventHandler<MentorHelpTicketsListMessage>? OnTicketsListReceived;
|
||||
public event EventHandler<MentorHelpTicketMessagesMessage>? OnTicketMessagesReceived;
|
||||
public event EventHandler<MentorHelpStatisticsMessage>? OnStatisticsReceived;
|
||||
public event EventHandler<MentorHelpOpenTicketMessage>? OnOpenTicketReceived;
|
||||
public event EventHandler<MentorHelpPlayerTypingUpdated>? OnPlayerTypingUpdated;
|
||||
private const double TypingUpdateResendIntervalSeconds = 1;
|
||||
private (TimeSpan Timestamp, bool Typing) _lastTypingUpdateSent;
|
||||
|
||||
|
||||
protected override void OnCreateTicketMessage(MentorHelpCreateTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
|
|
@ -59,6 +66,7 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
SubscribeNetworkEvent<MentorHelpTicketMessagesMessage>(OnTicketMessages);
|
||||
SubscribeNetworkEvent<MentorHelpStatisticsMessage>(OnStatistics);
|
||||
SubscribeNetworkEvent<MentorHelpOpenTicketMessage>(OnOpenTicket);
|
||||
SubscribeNetworkEvent<MentorHelpPlayerTypingUpdated>(OnTypingUpdated);
|
||||
}
|
||||
|
||||
private void OnOpenTicket(MentorHelpOpenTicketMessage message, EntitySessionEventArgs eventArgs)
|
||||
|
|
@ -66,6 +74,11 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
OnOpenTicketReceived?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnTypingUpdated(MentorHelpPlayerTypingUpdated message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnPlayerTypingUpdated?.Invoke(this, message);
|
||||
}
|
||||
|
||||
private void OnTicketUpdate(MentorHelpTicketUpdateMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
OnTicketUpdated?.Invoke(this, message);
|
||||
|
|
@ -150,5 +163,17 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
{
|
||||
RaiseNetworkEvent(new MentorHelpRequestStatisticsMessage());
|
||||
}
|
||||
|
||||
public void SendInputTextUpdated(int ticketId, bool typing)
|
||||
{
|
||||
if (_lastTypingUpdateSent.Typing == typing &&
|
||||
_lastTypingUpdateSent.Timestamp + TimeSpan.FromSeconds(TypingUpdateResendIntervalSeconds) > _gameTiming.RealTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastTypingUpdateSent = (_gameTiming.RealTime, typing);
|
||||
RaiseNetworkEvent(new MentorHelpClientTypingUpdated(ticketId, typing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ using Robust.Client.UserInterface.Controllers;
|
|||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp;
|
||||
|
||||
|
|
@ -30,17 +32,21 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[UISystemDependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
private MentorHelpSystem? _mentorHelpSystem;
|
||||
public IMentorHelpUIHandler? UIHelper;
|
||||
private bool _hasMentorPermissions;
|
||||
private bool _hasUnreadTickets;
|
||||
private readonly HashSet<int> _unreadTicketIds = new();
|
||||
private readonly Dictionary<int, int> _lastMessageIdByTicket = new();
|
||||
|
||||
// Последнее состояние тикетов. Нужны для фильтрации "моих" тикетов и авто-открытия.
|
||||
private readonly Dictionary<int, MentorHelpTicketData> _ticketDataById = new();
|
||||
private bool _mentorHelpSoundEnabled;
|
||||
private string? _mentorHelpSound;
|
||||
private static readonly SoundSpecifier? MentorHelpSound =
|
||||
new SoundPathSpecifier("/Audio/_Sunrise/Effects/adminticketopen.ogg", AudioParams.Default.WithVolume(-3f));
|
||||
|
||||
private Button? LobbyMHelpButton => (UIManager.ActiveScreen as LobbyGui)?.MHelpButton;
|
||||
private MenuButton? GameMHelpButton => UIManager.GetActiveUIWidgetOrNull<GameTopMenuBar>()?.MHelpButton;
|
||||
|
|
@ -52,7 +58,6 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
base.Initialize();
|
||||
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
_config.OnValueChanged(SunriseCCVars.MentorHelpSound, v => _mentorHelpSound = v, true); // Reuse ahelp sound for now
|
||||
_config.OnValueChanged(SunriseCCVars.MentorHelpSoundEnabled, v => _mentorHelpSoundEnabled = v, true);
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +68,7 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
_mentorHelpSystem.OnTicketsListReceived += OnTicketsListReceived;
|
||||
_mentorHelpSystem.OnTicketMessagesReceived += OnTicketMessagesReceived;
|
||||
_mentorHelpSystem.OnOpenTicketReceived += OnOpenTicketReceived;
|
||||
_mentorHelpSystem.OnPlayerTypingUpdated += OnPlayerTypingUpdated;
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenMentorHelp,
|
||||
|
|
@ -74,9 +80,23 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
{
|
||||
EnsureUIHelper();
|
||||
|
||||
// Open the window and instruct UI to open the specific ticket
|
||||
Open();
|
||||
UIHelper?.OpenTicket(message.TicketId);
|
||||
if (UIHelper == null)
|
||||
return;
|
||||
|
||||
if (!UIHelper.IsOpen)
|
||||
{
|
||||
UIHelper.OpenWindow();
|
||||
UIHelper.OpenTicket(message.TicketId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (UIHelper.CurrentTicketId == message.TicketId)
|
||||
{
|
||||
_mentorHelpSystem?.RequestTicketMessages(message.TicketId);
|
||||
return;
|
||||
}
|
||||
|
||||
UIHelper.OpenTicket(message.TicketId);
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(MentorHelpSystem system)
|
||||
|
|
@ -88,6 +108,8 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
_mentorHelpSystem.OnTicketUpdated -= OnTicketUpdated;
|
||||
_mentorHelpSystem.OnTicketsListReceived -= OnTicketsListReceived;
|
||||
_mentorHelpSystem.OnTicketMessagesReceived -= OnTicketMessagesReceived;
|
||||
_mentorHelpSystem.OnOpenTicketReceived -= OnOpenTicketReceived;
|
||||
_mentorHelpSystem.OnPlayerTypingUpdated -= OnPlayerTypingUpdated;
|
||||
_mentorHelpSystem = null;
|
||||
}
|
||||
|
||||
|
|
@ -100,33 +122,39 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
SubscribeToButtons();
|
||||
if (GameMHelpButton != null)
|
||||
{
|
||||
// Защита от повторной подписки, OnStateEntered может вызываться несколько раз
|
||||
// Аналогично в Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs (метод OnStateEntered)
|
||||
GameMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
GameMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
GameMHelpButton.Pressed = UIHelper?.IsOpen ?? false;
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
// Keep UI helper for potential return to game
|
||||
if (GameMHelpButton != null)
|
||||
GameMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
}
|
||||
|
||||
public void OnStateEntered(LobbyState state)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
SubscribeToButtons();
|
||||
if (LobbyMHelpButton != null)
|
||||
{
|
||||
// То же самое для лобби, см. Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs (метод OnStateEntered)
|
||||
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
LobbyMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
LobbyMHelpButton.Pressed = UIHelper?.IsOpen ?? false;
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateExited(LobbyState state)
|
||||
{
|
||||
// Keep UI helper for potential return to lobby
|
||||
}
|
||||
|
||||
private void SubscribeToButtons()
|
||||
{
|
||||
if (GameMHelpButton != null)
|
||||
GameMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
|
||||
if (LobbyMHelpButton != null)
|
||||
LobbyMHelpButton.OnPressed += MHelpButtonPressed;
|
||||
LobbyMHelpButton.OnPressed -= MHelpButtonPressed;
|
||||
}
|
||||
|
||||
private void MHelpButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
|
|
@ -136,9 +164,7 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
|
||||
private void OnAdminStatusUpdated()
|
||||
{
|
||||
_hasMentorPermissions = _adminManager.HasFlag(AdminFlags.Mentor);
|
||||
|
||||
if (UIHelper is not { IsOpen: true })
|
||||
if (UIHelper == null || !UIHelper.IsOpen)
|
||||
return;
|
||||
|
||||
EnsureUIHelper();
|
||||
|
|
@ -146,32 +172,125 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
|
||||
private void OnTicketUpdated(object? sender, MentorHelpTicketUpdateMessage message)
|
||||
{
|
||||
if (_mentorHelpSound != null && _mentorHelpSoundEnabled)
|
||||
{
|
||||
_audio.PlayGlobal(_mentorHelpSound, Filter.Local(), false);
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
// Новый тикет
|
||||
var isNewTicket = !_ticketDataById.ContainsKey(message.Ticket.Id);
|
||||
_ticketDataById[message.Ticket.Id] = message.Ticket;
|
||||
|
||||
EnsureUIHelper();
|
||||
|
||||
if (!UIHelper!.IsOpen)
|
||||
if (UIHelper == null)
|
||||
return;
|
||||
|
||||
// Звук для менторов только при появлении нового тикета
|
||||
if (isNewTicket && _mentorHelpSoundEnabled && _adminManager.HasFlag(AdminFlags.Mentor) && IsRelevantTicket(message.Ticket.Id))
|
||||
{
|
||||
UnreadTicketReceived();
|
||||
_audio.PlayGlobal(MentorHelpSound, Filter.Local(), false);
|
||||
|
||||
if (!UIHelper.IsOpen)
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
UIHelper!.TicketUpdated(message.Ticket);
|
||||
if (!IsRelevantTicket(message.Ticket.Id))
|
||||
_unreadTicketIds.Remove(message.Ticket.Id);
|
||||
|
||||
UpdateButtonStyling();
|
||||
|
||||
UIHelper.TicketUpdated(message.Ticket);
|
||||
|
||||
if (UIHelper.IsOpen && UIHelper.CurrentTicketId == message.Ticket.Id)
|
||||
{
|
||||
_mentorHelpSystem?.RequestTicketMessages(message.Ticket.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTicketsListReceived(object? sender, MentorHelpTicketsListMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper!.TicketsListReceived(message.Tickets);
|
||||
foreach (var ticket in message.Tickets)
|
||||
{
|
||||
_ticketDataById[ticket.Id] = ticket;
|
||||
if (!IsRelevantTicket(ticket.Id))
|
||||
_unreadTicketIds.Remove(ticket.Id);
|
||||
}
|
||||
UpdateButtonStyling();
|
||||
|
||||
if (UIHelper == null)
|
||||
return;
|
||||
|
||||
UIHelper.TicketsListReceived(message.Tickets);
|
||||
}
|
||||
|
||||
private void OnTicketMessagesReceived(object? sender, MentorHelpTicketMessagesMessage message)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
UIHelper!.TicketMessagesReceived(message.TicketId, message.Messages);
|
||||
|
||||
UpdateUnreadTickets(message.TicketId, message.Messages);
|
||||
|
||||
// Звук только на новые входящие сообщения
|
||||
TryPlaySoundForNewMessage(message.TicketId, message.Messages);
|
||||
|
||||
var autoOpen = _config.GetCVar(SunriseCCVars.MentorHelpAutoOpenOnNewMessage);
|
||||
var shouldAutoOpen = autoOpen && ShouldAutoOpenTicket(message.TicketId, message.Messages);
|
||||
|
||||
// Автооткрытие работает только когда окно закрыто, ибо раздражает постоянными переключениями
|
||||
if (shouldAutoOpen && UIHelper != null && !UIHelper.IsOpen)
|
||||
UIHelper.OpenTicket(message.TicketId);
|
||||
|
||||
if (UIHelper == null)
|
||||
return;
|
||||
|
||||
UIHelper.TicketMessagesReceived(message.TicketId, message.Messages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проигрывает звук только на новое входящее сообщение по тикету
|
||||
/// </summary>
|
||||
private void TryPlaySoundForNewMessage(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
if (!_mentorHelpSoundEnabled || messages.Count == 0)
|
||||
return;
|
||||
|
||||
var lastMessage = messages[^1];
|
||||
|
||||
if (!_lastMessageIdByTicket.TryGetValue(ticketId, out var previousMessageId))
|
||||
{
|
||||
_lastMessageIdByTicket[ticketId] = lastMessage.Id;
|
||||
return;
|
||||
}
|
||||
|
||||
// Не новое сообщение
|
||||
if (previousMessageId == lastMessage.Id)
|
||||
return;
|
||||
|
||||
_lastMessageIdByTicket[ticketId] = lastMessage.Id;
|
||||
|
||||
var localUser = _playerManager.LocalUser;
|
||||
|
||||
// Без звука свое сообщение
|
||||
if (localUser == null || lastMessage.SenderUserId == localUser.Value)
|
||||
return;
|
||||
|
||||
// Тикет не релевантентен
|
||||
if (!IsRelevantTicket(ticketId))
|
||||
return;
|
||||
|
||||
var isViewingTicket = UIHelper != null && UIHelper.IsOpen && UIHelper.CurrentTicketId == ticketId;
|
||||
var isWindowClosed = UIHelper != null && !UIHelper.IsOpen;
|
||||
|
||||
// Окно открыто, но мы не в этом тикете
|
||||
if (!isViewingTicket && !isWindowClosed)
|
||||
return;
|
||||
|
||||
// Новое входящее сообщение
|
||||
_audio.PlayGlobal(MentorHelpSound, Filter.Local(), false);
|
||||
|
||||
if (isWindowClosed)
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
private void OnPlayerTypingUpdated(object? sender, MentorHelpPlayerTypingUpdated message)
|
||||
{
|
||||
UIHelper?.PlayerTypingUpdated(message.TicketId, message.PlayerName, message.Typing);
|
||||
}
|
||||
|
||||
public void EnsureUIHelper()
|
||||
|
|
@ -182,7 +301,11 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
return;
|
||||
|
||||
UIHelper?.Dispose();
|
||||
var ownerUserId = _playerManager.LocalUser!.Value;
|
||||
var localUser = _playerManager.LocalUser;
|
||||
if (localUser == null)
|
||||
return;
|
||||
|
||||
var ownerUserId = localUser.Value;
|
||||
|
||||
UIHelper = hasMentorPerms
|
||||
? new MentorMentorHelpUIHandler(ownerUserId, _mentorHelpSystem)
|
||||
|
|
@ -196,8 +319,15 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
if (_playerManager.LocalUser == null)
|
||||
return;
|
||||
|
||||
EnsureUIHelper();
|
||||
UIHelper!.OpenWindow();
|
||||
if (UIHelper == null)
|
||||
return;
|
||||
|
||||
UIHelper.OpenWindow();
|
||||
|
||||
SetMentorHelpPressed(true);
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +343,7 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
private void SetMentorHelpPressed(bool pressed)
|
||||
{
|
||||
UIManager.ClickSound();
|
||||
UnreadTicketRead();
|
||||
UnreadTicketsRead();
|
||||
|
||||
if (GameMHelpButton != null)
|
||||
{
|
||||
|
|
@ -226,21 +356,88 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
}
|
||||
}
|
||||
|
||||
private void UnreadTicketReceived()
|
||||
private void UnreadTicketsRead()
|
||||
{
|
||||
_hasUnreadTickets = true;
|
||||
_unreadTicketIds.Clear();
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
|
||||
private void UnreadTicketRead()
|
||||
private void UpdateUnreadTickets(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
_hasUnreadTickets = false;
|
||||
var localUser = _playerManager.LocalUser;
|
||||
if (localUser == null)
|
||||
return;
|
||||
|
||||
if (!IsRelevantTicket(ticketId))
|
||||
{
|
||||
_unreadTicketIds.Remove(ticketId);
|
||||
UpdateButtonStyling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (messages.Count == 0)
|
||||
{
|
||||
_unreadTicketIds.Remove(ticketId);
|
||||
UpdateButtonStyling();
|
||||
return;
|
||||
}
|
||||
|
||||
var lastMessage = messages[^1];
|
||||
var isIncoming = lastMessage.SenderUserId != localUser.Value;
|
||||
var isViewingTicket = UIHelper != null && UIHelper.IsOpen && UIHelper.CurrentTicketId == ticketId;
|
||||
var unread = isIncoming && !isViewingTicket;
|
||||
|
||||
if (unread)
|
||||
_unreadTicketIds.Add(ticketId);
|
||||
else
|
||||
_unreadTicketIds.Remove(ticketId);
|
||||
|
||||
UpdateButtonStyling();
|
||||
}
|
||||
|
||||
private bool IsRelevantTicket(int ticketId)
|
||||
{
|
||||
var localUser = _playerManager.LocalUser;
|
||||
if (localUser == null || !_ticketDataById.TryGetValue(ticketId, out var ticket))
|
||||
return false;
|
||||
|
||||
var localId = localUser.Value;
|
||||
var isMentor = _adminManager.HasFlag(AdminFlags.Mentor);
|
||||
|
||||
if (isMentor)
|
||||
return ticket.AssignedToUserId == null || ticket.AssignedToUserId == localId;
|
||||
|
||||
return ticket.PlayerId == localId;
|
||||
}
|
||||
|
||||
private bool ShouldAutoOpenTicket(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
var localUser = _playerManager.LocalUser;
|
||||
if (localUser == null || !_ticketDataById.TryGetValue(ticketId, out var ticket))
|
||||
return false;
|
||||
|
||||
var localId = localUser.Value;
|
||||
var isOwner = ticket.PlayerId == localId;
|
||||
var isAssignedToMe = ticket.AssignedToUserId == localId;
|
||||
|
||||
// Авто-открытие только у автора тикета и назначенного ментора
|
||||
if (!isOwner && !isAssignedToMe)
|
||||
return false;
|
||||
|
||||
if (messages.Count == 0)
|
||||
return false;
|
||||
|
||||
var lastMessage = messages[^1];
|
||||
return lastMessage.SenderUserId != localId;
|
||||
}
|
||||
|
||||
private void UpdateButtonStyling()
|
||||
{
|
||||
if (_hasUnreadTickets)
|
||||
var unreadCount = _unreadTicketIds.Count;
|
||||
var hasUnread = unreadCount > 0;
|
||||
var displayCount = unreadCount;
|
||||
|
||||
if (hasUnread)
|
||||
{
|
||||
GameMHelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
|
||||
LobbyMHelpButton?.StyleClasses.Add("ButtonColorRed");
|
||||
|
|
@ -250,6 +447,18 @@ public sealed class MentorHelpUIController : UIController, IOnSystemChanged<Ment
|
|||
GameMHelpButton?.StyleClasses.Remove(MenuButton.StyleClassRedTopButton);
|
||||
LobbyMHelpButton?.StyleClasses.Remove("ButtonColorRed");
|
||||
}
|
||||
|
||||
if (LobbyMHelpButton != null)
|
||||
{
|
||||
var baseText = _loc.GetString("ui-lobby-mhelp-button");
|
||||
LobbyMHelpButton.Text = displayCount > 0 ? $"{baseText} ({displayCount})" : baseText;
|
||||
}
|
||||
|
||||
if (GameMHelpButton != null)
|
||||
{
|
||||
var baseTooltip = _loc.GetString("ui-options-function-open-mentor-help");
|
||||
GameMHelpButton.ToolTip = displayCount > 0 ? $"{baseTooltip} ({displayCount})" : baseTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +469,7 @@ public interface IMentorHelpUIHandler : IDisposable
|
|||
{
|
||||
bool IsOpen { get; }
|
||||
bool HasMentorPermissions { get; }
|
||||
int? CurrentTicketId { get; }
|
||||
event Action? OnClose;
|
||||
|
||||
void OpenWindow();
|
||||
|
|
@ -268,6 +478,7 @@ public interface IMentorHelpUIHandler : IDisposable
|
|||
void TicketUpdated(MentorHelpTicketData ticket);
|
||||
void TicketsListReceived(List<MentorHelpTicketData> tickets);
|
||||
void TicketMessagesReceived(int ticketId, List<MentorHelpMessageData> messages);
|
||||
void PlayerTypingUpdated(int ticketId, string playerName, bool typing);
|
||||
}
|
||||
|
||||
public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
||||
|
|
@ -275,6 +486,7 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
public bool IsOpen { get; private set; }
|
||||
public bool HasMentorPermissions => false;
|
||||
public event Action? OnClose;
|
||||
public int? CurrentTicketId { get; private set; }
|
||||
|
||||
private readonly NetUserId _ownerUserId;
|
||||
private readonly MentorHelpSystem? _mentorHelpSystem;
|
||||
|
|
@ -300,6 +512,7 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
_window.OnClose += () =>
|
||||
{
|
||||
IsOpen = false;
|
||||
CurrentTicketId = null;
|
||||
OnClose?.Invoke();
|
||||
_window = null;
|
||||
};
|
||||
|
|
@ -312,6 +525,7 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
|
||||
public void OpenTicket(int ticketId)
|
||||
{
|
||||
CurrentTicketId = ticketId;
|
||||
// Ensure window is open
|
||||
OpenWindow();
|
||||
// Ask control to focus the ticket if possible
|
||||
|
|
@ -322,9 +536,14 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
|
||||
public void CloseWindow()
|
||||
{
|
||||
_window?.Close();
|
||||
IsOpen = false;
|
||||
OnClose?.Invoke();
|
||||
CurrentTicketId = null;
|
||||
if (_window == null)
|
||||
{
|
||||
IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
public void TicketUpdated(MentorHelpTicketData ticket)
|
||||
|
|
@ -342,6 +561,12 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
_window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
|
||||
}
|
||||
|
||||
// Обновление статуса печати, все еще печатает ли игрок
|
||||
public void PlayerTypingUpdated(int ticketId, string playerName, bool typing)
|
||||
{
|
||||
_window?.MentorHelp.UpdatePlayerTyping(ticketId, playerName, typing);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloseWindow();
|
||||
|
|
@ -353,6 +578,7 @@ public sealed class PlayerMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
/// </summary>
|
||||
public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
||||
{
|
||||
public int? CurrentTicketId { get; private set; }
|
||||
public bool IsOpen { get; private set; }
|
||||
public bool HasMentorPermissions => true;
|
||||
public event Action? OnClose;
|
||||
|
|
@ -381,6 +607,7 @@ public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
_window.OnClose += () =>
|
||||
{
|
||||
IsOpen = false;
|
||||
CurrentTicketId = null;
|
||||
OnClose?.Invoke();
|
||||
_window = null;
|
||||
};
|
||||
|
|
@ -393,6 +620,7 @@ public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
|
||||
public void OpenTicket(int ticketId)
|
||||
{
|
||||
CurrentTicketId = ticketId;
|
||||
OpenWindow();
|
||||
_window?.MentorHelp.TryOpenTicket(ticketId);
|
||||
_mentorHelpSystem?.RequestTicketMessages(ticketId);
|
||||
|
|
@ -400,9 +628,14 @@ public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
|
||||
public void CloseWindow()
|
||||
{
|
||||
_window?.Close();
|
||||
IsOpen = false;
|
||||
OnClose?.Invoke();
|
||||
CurrentTicketId = null;
|
||||
if (_window == null)
|
||||
{
|
||||
IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
public void TicketUpdated(MentorHelpTicketData ticket)
|
||||
|
|
@ -420,6 +653,12 @@ public sealed class MentorMentorHelpUIHandler : IMentorHelpUIHandler
|
|||
_window?.MentorHelp.UpdateTicketMessages(ticketId, messages);
|
||||
}
|
||||
|
||||
// Обновление статуса печати, все еще печатает ли ментор
|
||||
public void PlayerTypingUpdated(int ticketId, string playerName, bool typing)
|
||||
{
|
||||
_window?.MentorHelp.UpdatePlayerTyping(ticketId, playerName, typing);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CloseWindow();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
|
|
@ -13,6 +14,8 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
public MentorHelpWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MinSize = new Vector2(650, 450);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,31 @@
|
|||
Name="BackgroundColorPanel"
|
||||
MouseFilter="Stop">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<customControls:HSeparator/>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 2">
|
||||
<customControls:VSeparator/>
|
||||
|
||||
<Label Name="MentorNameLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
|
||||
<Label Name="TicketsClaimedLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
|
||||
<Label Name="MessagesCountLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator/>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -7,38 +7,32 @@
|
|||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 2">
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="IdLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="PlayerLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="StatusLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
Align="Center"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="AssignedLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="SubjectLabel" Access="Public"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<customControls:VSeparator/>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator/>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,37 @@ using Robust.Client.AutoGenerated;
|
|||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client._Sunrise.MentorHelp
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TicketEntryControl : Control
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private static readonly Color NormalColor = Color.FromHex("#202023");
|
||||
private static readonly Color HoverColor = Color.FromHex("#2F2F33");
|
||||
private static readonly Color UnreadColor = Color.FromHex("#4A1F1F");
|
||||
private static readonly Color AuthorUnreadColor = Color.FromHex("#17395C");
|
||||
private static readonly Color UnassignedColor = Color.FromHex("#1E3A1E");
|
||||
private static readonly Color ClosedColor = Color.FromHex("#3B0F0F");
|
||||
private static readonly Color AwaitingColor = Color.FromHex("#3A2F12");
|
||||
private static readonly Color UnassignedAssigneeLabelColor = Color.FromHex("#7CFF7C");
|
||||
|
||||
private const float HoverColorLerp = 0.06f;
|
||||
|
||||
private MentorHelpTicketData? _ticketData;
|
||||
private bool _newMessageFromAuthor;
|
||||
private bool _isHovering;
|
||||
|
||||
public event Action<MentorHelpTicketData>? OnTicketSelected;
|
||||
|
||||
public TicketEntryControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
TooltipDelay = 0.5f;
|
||||
|
||||
BackgroundColorPanel.PanelOverride = new StyleBoxFlat
|
||||
|
|
@ -30,14 +44,14 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
|
||||
BackgroundColorPanel.OnMouseEntered += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = HoverColor;
|
||||
_isHovering = true;
|
||||
UpdateBackgroundColor();
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnMouseExited += args =>
|
||||
{
|
||||
var panel = (StyleBoxFlat)BackgroundColorPanel.PanelOverride!;
|
||||
panel.BackgroundColor = NormalColor;
|
||||
_isHovering = false;
|
||||
UpdateBackgroundColor();
|
||||
};
|
||||
|
||||
BackgroundColorPanel.OnKeyBindDown += args =>
|
||||
|
|
@ -57,26 +71,88 @@ namespace Content.Client._Sunrise.MentorHelp
|
|||
IdLabel.Text = $"#{ticketData.Id}";
|
||||
PlayerLabel.Text = ticketData.PlayerName;
|
||||
|
||||
var statusText = GetStatusText(ticketData.Status);
|
||||
AssignedLabel.Text = ticketData.AssignedToName ?? _loc.GetString("mentor-help-unassigned");
|
||||
AssignedLabel.FontColorOverride = ticketData.AssignedToUserId == null
|
||||
&& ticketData.Status != MentorHelpTicketStatus.Closed
|
||||
? UnassignedAssigneeLabelColor
|
||||
: null;
|
||||
|
||||
SubjectLabel.Text = ticketData.Subject;
|
||||
UpdateStatusLabel();
|
||||
UpdateBackgroundColor();
|
||||
}
|
||||
|
||||
public void SetNewMessageFromAuthor(bool newMessageFromAuthor)
|
||||
{
|
||||
if (_newMessageFromAuthor == newMessageFromAuthor)
|
||||
return;
|
||||
|
||||
_newMessageFromAuthor = newMessageFromAuthor;
|
||||
UpdateStatusLabel();
|
||||
UpdateBackgroundColor();
|
||||
}
|
||||
|
||||
private void UpdateStatusLabel()
|
||||
{
|
||||
if (_ticketData == null)
|
||||
return;
|
||||
|
||||
var statusText = GetStatusText(_ticketData.Status);
|
||||
StatusLabel.Text = statusText;
|
||||
}
|
||||
|
||||
AssignedLabel.Text = ticketData.AssignedToName ?? Loc.GetString("mentor-help-unassigned");
|
||||
private void UpdateBackgroundColor()
|
||||
{
|
||||
if (BackgroundColorPanel.PanelOverride is not StyleBoxFlat panel || _ticketData == null)
|
||||
return;
|
||||
|
||||
var isUnread = _ticketData.HasUnreadMessages;
|
||||
var isAuthorUnread = isUnread && _newMessageFromAuthor;
|
||||
var isUnassigned = !isUnread && _ticketData.AssignedToUserId == null;
|
||||
var isClosed = _ticketData.Status == MentorHelpTicketStatus.Closed;
|
||||
var isAwaiting = _ticketData.Status == MentorHelpTicketStatus.AwaitingResponse;
|
||||
|
||||
var normal = GetNormalBackgroundColor(isClosed, isAwaiting, isAuthorUnread, isUnread, isUnassigned);
|
||||
var hover = Color.InterpolateBetween(normal, Color.White, HoverColorLerp);
|
||||
|
||||
panel.BackgroundColor = _isHovering ? hover : normal;
|
||||
}
|
||||
|
||||
private static Color GetNormalBackgroundColor(
|
||||
bool isClosed,
|
||||
bool isAwaiting,
|
||||
bool isAuthorUnread,
|
||||
bool isUnread,
|
||||
bool isUnassigned)
|
||||
{
|
||||
if (isClosed)
|
||||
return ClosedColor;
|
||||
|
||||
if (isAwaiting)
|
||||
return AwaitingColor;
|
||||
|
||||
if (isAuthorUnread)
|
||||
return AuthorUnreadColor;
|
||||
|
||||
if (isUnread)
|
||||
return UnreadColor;
|
||||
|
||||
if (isUnassigned)
|
||||
return UnassignedColor;
|
||||
|
||||
return NormalColor;
|
||||
|
||||
var subjectText = ticketData.Subject;
|
||||
if (ticketData.HasUnreadMessages)
|
||||
subjectText = "* " + subjectText;
|
||||
SubjectLabel.Text = subjectText;
|
||||
}
|
||||
|
||||
private string GetStatusText(MentorHelpTicketStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
MentorHelpTicketStatus.Open => Loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => Loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => Loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => Loc.GetString("mentor-help-status-closed"),
|
||||
_ => Loc.GetString("mentor-help-status-unknown")
|
||||
MentorHelpTicketStatus.Open => _loc.GetString("mentor-help-status-open"),
|
||||
MentorHelpTicketStatus.Assigned => _loc.GetString("mentor-help-status-assigned"),
|
||||
MentorHelpTicketStatus.AwaitingResponse => _loc.GetString("mentor-help-status-awaiting"),
|
||||
MentorHelpTicketStatus.Closed => _loc.GetString("mentor-help-status-closed"),
|
||||
_ => _loc.GetString("mentor-help-status-unknown")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
|
||||
private ISharedSponsorsManager? _sponsorsManager; // Sunrise-Sponsors
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private List<MentorHelpStatisticsData>? _mentorStatsCache;
|
||||
private DateTimeOffset? _mentorStatsCacheTime;
|
||||
private readonly float _mentorCacheInterval = 10;
|
||||
|
|
@ -46,8 +44,6 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("MHELP");
|
||||
|
||||
_rateLimit.Register(
|
||||
RateLimitKey,
|
||||
new RateLimitRegistration(SunriseCCVars.MentorHelpRateLimitPeriod, // Reuse ahelp rate limit config
|
||||
|
|
@ -55,13 +51,15 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
PlayerRateLimitedAction)
|
||||
);
|
||||
|
||||
SubscribeNetworkEvent<MentorHelpClientTypingUpdated>(OnClientTypingUpdated);
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
IoCManager.Instance!.TryResolveType(out _sponsorsManager); // Sunrise-Sponsors
|
||||
}
|
||||
|
||||
private void PlayerRateLimitedAction(ICommonSession session)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) was rate limited for mentor help");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) was rate limited for mentor help");
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
|
|
@ -81,13 +79,13 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
// Validate input
|
||||
if (string.IsNullOrWhiteSpace(message.Subject) || string.IsNullOrWhiteSpace(message.Message))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with empty subject or message");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with empty subject or message");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Subject.Length > 256 || message.Message.Length > 4096)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with too long subject or message");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to create mentor help ticket with too long subject or message");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +94,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var now = DateTimeOffset.UtcNow;
|
||||
var ticket = new MentorHelpTicket
|
||||
{
|
||||
PlayerId = session.UserId.UserId,
|
||||
PlayerId = session.UserId,
|
||||
Subject = message.Subject.Trim(),
|
||||
Status = MentorHelpTicketStatus.Open,
|
||||
CreatedAt = now,
|
||||
|
|
@ -117,20 +115,19 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
};
|
||||
await _dbManager.AddMentorHelpMessageAsync(ticketMessage);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) created mentor help ticket #{ticket.Id}: {ticket.Subject}");
|
||||
Log.Info($"Player {session.Name} ({session.UserId}) created mentor help ticket #{ticket.Id}: {ticket.Subject}");
|
||||
|
||||
// Notify player
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), session.Channel);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
// Instruct the player's client to open the newly created ticket
|
||||
RaiseNetworkEvent(new MentorHelpOpenTicketMessage(ticket.Id), session.Channel);
|
||||
|
||||
// Notify mentors/admins
|
||||
await NotifyMentorsOfNewTicket(ticketData);
|
||||
var messageData = await ConvertToMessageDataAsync(ticketMessage);
|
||||
await NotifyTicketMessage(ticketData, messageData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error creating mentor help ticket for {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error creating mentor help ticket for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +138,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
// Check permissions
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to claim mentor help ticket without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to claim mentor help ticket without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -150,13 +147,18 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim non-existent ticket #{message.TicketId}");
|
||||
Log.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim closed ticket #{message.TicketId}");
|
||||
Log.Warning($"Mentor {session.Name} ({session.UserId}) tried to claim closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.AssignedToUserId.HasValue && ticket.AssignedToUserId.Value != session.UserId.UserId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +169,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Mentor {session.Name} ({session.UserId}) claimed ticket #{ticket.Id}");
|
||||
Log.Info($"Mentor {session.Name} ({session.UserId}) claimed ticket #{ticket.Id}");
|
||||
|
||||
// Notify all relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
|
|
@ -175,7 +177,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error claiming mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error claiming mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,13 +194,13 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to non-existent ticket #{message.TicketId}");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to reply to non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to closed ticket #{message.TicketId}");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to reply to closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -208,21 +210,21 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
if (!isTicketOwner && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to reply to ticket #{message.TicketId} without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to reply to ticket #{message.TicketId} without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Staff-only messages can only be sent by mentors/admins
|
||||
if (message.IsStaffOnly && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to send staff-only message without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to send staff-only message without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if (string.IsNullOrWhiteSpace(message.Message) || message.Message.Length > 4096)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to send invalid message to ticket #{message.TicketId}");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to send invalid message to ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -259,16 +261,27 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
ticket.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) replied to ticket #{message.TicketId}");
|
||||
Log.Info($"Player {session.Name} ({session.UserId}) replied to ticket #{message.TicketId}");
|
||||
|
||||
// Notify relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
var messageData = await ConvertToMessageDataAsync(ticketMessage);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
await NotifyTicketMessage(ticketData, messageData);
|
||||
|
||||
if (hasMentorPerms)
|
||||
{
|
||||
var userId = new NetUserId(ticket.PlayerId);
|
||||
|
||||
if (_playerManager.TryGetSessionById(userId, out var authorSession))
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpOpenTicketMessage(ticket.Id), authorSession.Channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error adding reply to mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error adding reply to mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,13 +294,13 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close non-existent ticket #{message.TicketId}");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to close non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close already closed ticket #{message.TicketId}");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to close already closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +310,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
if (!isTicketOwner && !hasMentorPerms)
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to close ticket #{message.TicketId} without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to close ticket #{message.TicketId} without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +322,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Player {session.Name} ({session.UserId}) closed ticket #{ticket.Id}");
|
||||
Log.Info($"Player {session.Name} ({session.UserId}) closed ticket #{ticket.Id}");
|
||||
|
||||
// Notify relevant parties
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
|
|
@ -317,7 +330,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error closing mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error closing mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +352,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
// Mentor/admin requesting all tickets (both open and closed)
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to request all mentor help tickets without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to request all mentor help tickets without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -359,7 +372,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help tickets for {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error requesting mentor help tickets for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +382,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to unassign mentor help ticket without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to unassign mentor help ticket without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -378,13 +391,13 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign non-existent ticket #{message.TicketId}");
|
||||
Log.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.Status == MentorHelpTicketStatus.Closed)
|
||||
{
|
||||
_sawmill.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign closed ticket #{message.TicketId}");
|
||||
Log.Warning($"Mentor {session.Name} ({session.UserId}) tried to unassign closed ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -394,14 +407,14 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
await _dbManager.UpdateMentorHelpTicketAsync(ticket);
|
||||
|
||||
_sawmill.Info($"Mentor {session.Name} ({session.UserId}) unassigned ticket #{ticket.Id}");
|
||||
Log.Info($"Mentor {session.Name} ({session.UserId}) unassigned ticket #{ticket.Id}");
|
||||
|
||||
var ticketData = await ConvertToTicketDataAsync(ticket);
|
||||
await NotifyTicketUpdate(ticketData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error unassigning mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error unassigning mentor help ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -423,7 +436,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
|
||||
if (!HasMentorPermissions(session))
|
||||
{
|
||||
_sawmill.Warning($"Player {session.Name} ({session.UserId}) tried to request mentor help statistics without permissions");
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to request mentor help statistics without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -466,7 +479,7 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help statistics for {session.Name} ({session.UserId}): {ex}");
|
||||
Log.Error($"Error requesting mentor help statistics for {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -474,25 +487,44 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
{
|
||||
var session = eventArgs.SenderSession;
|
||||
|
||||
_sawmill.Info("Received RequestTicketMessages for ticket #{0} from {1} ({2})", message.TicketId, session.Name, session.UserId);
|
||||
Log.Info("Received RequestTicketMessages for ticket #{0} from {1} ({2})", message.TicketId, session.Name, session.UserId);
|
||||
|
||||
try
|
||||
{
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(message.TicketId);
|
||||
if (ticket == null)
|
||||
{
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to request messages for non-existent ticket #{message.TicketId}");
|
||||
return;
|
||||
}
|
||||
|
||||
var hasMentorPerms = HasMentorPermissions(session);
|
||||
var isTicketOwner = ticket.PlayerId == session.UserId.UserId;
|
||||
|
||||
if (!hasMentorPerms && !isTicketOwner)
|
||||
{
|
||||
Log.Warning($"Player {session.Name} ({session.UserId}) tried to request messages for ticket #{message.TicketId} without permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
var allMessages = await _dbManager.GetMentorHelpMessagesByTicketAsync(message.TicketId);
|
||||
var messageDatas = new List<MentorHelpMessageData>();
|
||||
foreach (var msg in allMessages.OrderBy(m => m.SentAt))
|
||||
{
|
||||
if (!hasMentorPerms && msg.IsStaffOnly)
|
||||
continue;
|
||||
|
||||
messageDatas.Add(await ConvertToMessageDataAsync(msg));
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new MentorHelpTicketMessagesMessage(message.TicketId, messageDatas), session.Channel);
|
||||
_sawmill.Info("Sent {0} messages for ticket #{1} to {2} ({3})", messageDatas.Count, message.TicketId, session.Name, session.UserId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error requesting mentor help messages for ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
Log.Info("Sent {0} messages for ticket #{1} to {2} ({3})", messageDatas.Count, message.TicketId, session.Name, session.UserId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"Error requesting mentor help messages for ticket #{message.TicketId} by {session.Name} ({session.UserId}): {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<MentorHelpTicketData> ConvertToTicketDataAsync(MentorHelpTicket ticket)
|
||||
|
|
@ -587,21 +619,12 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
var name = playerData?.LastSeenUserName;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Logger.Warning($"GetPlayerNameAsync: No name found for userId {userId}, returning 'Unknown'.");
|
||||
Log.Warning($"GetPlayerNameAsync: No name found for userId {userId}, returning 'Unknown'.");
|
||||
return "Unknown";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private async Task NotifyMentorsOfNewTicket(MentorHelpTicketData ticketData)
|
||||
{
|
||||
var mentors = GetTargetMentors();
|
||||
foreach (var mentor in mentors)
|
||||
{
|
||||
RaiseNetworkEvent(new MentorHelpTicketUpdateMessage(ticketData), mentor);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyTicketUpdate(MentorHelpTicketData ticketData)
|
||||
{
|
||||
// Notify the player
|
||||
|
|
@ -648,6 +671,32 @@ namespace Content.Server._Sunrise.MentorHelp
|
|||
.Select(p => p.Channel)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async void OnClientTypingUpdated(MentorHelpClientTypingUpdated msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var session = args.SenderSession;
|
||||
var ticket = await _dbManager.GetMentorHelpTicketAsync(msg.TicketId);
|
||||
if (ticket == null)
|
||||
return;
|
||||
|
||||
var update = new MentorHelpPlayerTypingUpdated(msg.TicketId, session.UserId, session.Name, msg.Typing);
|
||||
var recipients = new HashSet<INetChannel>();
|
||||
|
||||
if (_playerManager.TryGetSessionById(new NetUserId(ticket.PlayerId), out var authorSession))
|
||||
{
|
||||
if (!authorSession.UserId.Equals(session.UserId))
|
||||
recipients.Add(authorSession.Channel);
|
||||
}
|
||||
|
||||
if (ticket.AssignedToUserId.HasValue &&
|
||||
_playerManager.TryGetSessionById(new NetUserId(ticket.AssignedToUserId.Value), out var mentorSession))
|
||||
{
|
||||
if (!mentorSession.UserId.Equals(session.UserId))
|
||||
recipients.Add(mentorSession.Channel);
|
||||
}
|
||||
|
||||
foreach (var recipient in recipients)
|
||||
RaiseNetworkEvent(update, recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,134 +44,85 @@ namespace Content.Shared._Sunrise.MentorHelp
|
|||
/// Message to create a new mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpCreateTicketMessage : EntityEventArgs
|
||||
public sealed class MentorHelpCreateTicketMessage(string subject, string message) : EntityEventArgs
|
||||
{
|
||||
public string Subject { get; }
|
||||
public string Message { get; }
|
||||
|
||||
public MentorHelpCreateTicketMessage(string subject, string message)
|
||||
{
|
||||
Subject = subject;
|
||||
Message = message;
|
||||
}
|
||||
public readonly string Subject = subject;
|
||||
public readonly string Message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to claim a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpClaimTicketMessage : EntityEventArgs
|
||||
public sealed class MentorHelpClaimTicketMessage(int ticketId) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpClaimTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to reply to a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpReplyMessage : EntityEventArgs
|
||||
public sealed class MentorHelpReplyMessage(int ticketId, string message, bool isStaffOnly = false) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
public string Message { get; }
|
||||
public bool IsStaffOnly { get; }
|
||||
|
||||
public MentorHelpReplyMessage(int ticketId, string message, bool isStaffOnly = false)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
Message = message;
|
||||
IsStaffOnly = isStaffOnly;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
public readonly string Message = message;
|
||||
public readonly bool IsStaffOnly = isStaffOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to unassign a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpUnassignTicketMessage : EntityEventArgs
|
||||
public sealed class MentorHelpUnassignTicketMessage(int ticketId) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpUnassignTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to close a mentor help ticket
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpCloseTicketMessage : EntityEventArgs
|
||||
public sealed class MentorHelpCloseTicketMessage(int ticketId) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpCloseTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to request tickets (from client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpRequestTicketsMessage : EntityEventArgs
|
||||
public sealed class MentorHelpRequestTicketsMessage(bool onlyMine = false) : EntityEventArgs
|
||||
{
|
||||
public bool OnlyMine { get; }
|
||||
|
||||
public MentorHelpRequestTicketsMessage(bool onlyMine = false)
|
||||
{
|
||||
OnlyMine = onlyMine;
|
||||
}
|
||||
public readonly bool OnlyMine = onlyMine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with ticket update (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketUpdateMessage : EntityEventArgs
|
||||
public sealed class MentorHelpTicketUpdateMessage(MentorHelpTicketData ticket) : EntityEventArgs
|
||||
{
|
||||
public MentorHelpTicketData Ticket { get; }
|
||||
|
||||
public MentorHelpTicketUpdateMessage(MentorHelpTicketData ticket)
|
||||
{
|
||||
Ticket = ticket;
|
||||
}
|
||||
public readonly MentorHelpTicketData Ticket = ticket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with tickets list (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketsListMessage : EntityEventArgs
|
||||
public sealed class MentorHelpTicketsListMessage(List<MentorHelpTicketData> tickets) : EntityEventArgs
|
||||
{
|
||||
public List<MentorHelpTicketData> Tickets { get; }
|
||||
|
||||
public MentorHelpTicketsListMessage(List<MentorHelpTicketData> tickets)
|
||||
{
|
||||
Tickets = tickets;
|
||||
}
|
||||
public readonly List<MentorHelpTicketData> Tickets = tickets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message with ticket messages (to client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpTicketMessagesMessage : EntityEventArgs
|
||||
public sealed class MentorHelpTicketMessagesMessage(int ticketId, List<MentorHelpMessageData> messages) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
public List<MentorHelpMessageData> Messages { get; }
|
||||
|
||||
public MentorHelpTicketMessagesMessage(int ticketId, List<MentorHelpMessageData> messages)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
Messages = messages;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
public readonly List<MentorHelpMessageData> Messages = messages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -236,27 +187,18 @@ namespace Content.Shared._Sunrise.MentorHelp
|
|||
/// Сообщение с результатами статистики по менторам
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpStatisticsMessage : EntityEventArgs
|
||||
public sealed class MentorHelpStatisticsMessage(List<MentorHelpStatisticsData> statistics) : EntityEventArgs
|
||||
{
|
||||
public List<MentorHelpStatisticsData> Statistics { get; }
|
||||
public MentorHelpStatisticsMessage(List<MentorHelpStatisticsData> statistics)
|
||||
{
|
||||
Statistics = statistics;
|
||||
}
|
||||
public readonly List<MentorHelpStatisticsData> Statistics = statistics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to request messages for a specific ticket (from client)
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpRequestTicketMessagesMessage : EntityEventArgs
|
||||
public sealed class MentorHelpRequestTicketMessagesMessage(int ticketId) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
|
||||
public MentorHelpRequestTicketMessagesMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
public readonly int TicketId = ticketId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -264,13 +206,24 @@ namespace Content.Shared._Sunrise.MentorHelp
|
|||
/// This is used immediately after creating a ticket so the creating player sees their new ticket.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpOpenTicketMessage : EntityEventArgs
|
||||
public sealed class MentorHelpOpenTicketMessage(int ticketId) : EntityEventArgs
|
||||
{
|
||||
public int TicketId { get; }
|
||||
public readonly int TicketId = ticketId;
|
||||
}
|
||||
|
||||
public MentorHelpOpenTicketMessage(int ticketId)
|
||||
{
|
||||
TicketId = ticketId;
|
||||
}
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpClientTypingUpdated(int ticketId, bool typing) : EntityEventArgs
|
||||
{
|
||||
public readonly int TicketId = ticketId;
|
||||
public readonly bool Typing = typing;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MentorHelpPlayerTypingUpdated(int ticketId, NetUserId userId, string playerName, bool typing) : EntityEventArgs
|
||||
{
|
||||
public readonly int TicketId = ticketId;
|
||||
public readonly NetUserId UserId = userId;
|
||||
public readonly string PlayerName = playerName;
|
||||
public readonly bool Typing = typing;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -581,9 +581,12 @@ public sealed partial class SunriseCCVars : CVars
|
|||
public static readonly CVarDef<int> MentorHelpRateLimitCount =
|
||||
CVarDef.Create("mentor_help.rate_limit_count", 10, CVar.SERVERONLY);
|
||||
|
||||
public static readonly CVarDef<string> MentorHelpSound =
|
||||
CVarDef.Create("mentor_help.mentor_help_sound", "/Audio/_Sunrise/Effects/adminticketopen.ogg", CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
public static readonly CVarDef<bool> MentorHelpSoundEnabled =
|
||||
CVarDef.Create("mentor_help.mentor_help_sound_enabled", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Авто-открывать тикет при получении нового сообщения (только для автора и назначенного ментора).
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> MentorHelpAutoOpenOnNewMessage =
|
||||
CVarDef.Create("mentor_help.auto_open_on_new_message", false, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
ui-lobby-mhelp-button = MHelp
|
||||
ui-options-function-open-mentor-help = Открыть ментор помощь
|
||||
ui-options-function-open-mentor-help = Открыть Ментор помощь
|
||||
ui-options-function-open-help-choice = Открыть выбор помощи
|
||||
|
||||
# Mentor Help Window
|
||||
|
|
@ -12,13 +12,18 @@ mentor-help-tab-closed = Закрытые
|
|||
# Buttons
|
||||
mentor-help-statistics = Статистика
|
||||
mentor-help-new-ticket = Новый тикет
|
||||
mentor-help-back-to-list = Назад к списку
|
||||
mentor-help-back-to-list = Назад
|
||||
mentor-help-send-reply = Отправить
|
||||
mentor-help-claim = Взять
|
||||
mentor-help-unassign = Освободить
|
||||
mentor-help-close-ticket = Закрыть
|
||||
mentor-help-close-confirm = Вы уверены?
|
||||
mentor-help-cancel = Отмена
|
||||
mentor-help-close = Закрыть
|
||||
mentor-help-auto-open-tickets = Авто-открытие
|
||||
mentor-help-auto-open-tickets-tooltip = Автоматически открывать тикет при новых сообщениях
|
||||
mentor-help-ticket-activity-unread = Новое сообщение
|
||||
mentor-help-ticket-activity-author = Игрок написал
|
||||
|
||||
# Table columns
|
||||
mentor-help-column-id = ID
|
||||
|
|
@ -70,3 +75,9 @@ help-choice-ahelp-button = Админ-помощь
|
|||
help-choice-mhelp-button = Ментор-помощь
|
||||
help-choice-ahelp-desc-label = [color=#CCCCCC][font size=12]• Админ-помощь - для жалоб на игроков, сообщений о багах и нарушениях правил[/font][/color]
|
||||
help-choice-mhelp-desc-label = [color=#CCCCCC][font size=12]• Ментор-помощь - для вопросов о механиках игры и помощи новичкам[/font][/color]
|
||||
|
||||
help-kwoink-play-sound = Звук сообщений
|
||||
|
||||
mentor-help-statistics-column-mentor = Ментор
|
||||
mentor-help-statistics-column-tickets = Взятых тикетов
|
||||
mentor-help-statistics-column-messages = Сообщений
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue