Various Improvements to NanoChat (#2922)
* Port improvements to NanoChat from Einstein-Engines and Goob-Station * fix duplicate translation key * add missing space * Properly mark DeltaV changes * Allow muting individual NanoChat users, NanoChat UI to put per-chat buttons next to the message box * remove leftover from testing stuff * cycle through inputs with tab, confirm with enter; for new and edit chat * Add channel switching with (Shift+)Alt+Up/Down; Discord-Style * better null check * another better null check * Implement changes from ImpStation PR * Rename ContactControl -> ContactContainer * Requested changes * Move Loc to _DV, don't register system as manager * I'm so smart :) --------- Co-authored-by: Alex C <alex91905@yahoo.com>
This commit is contained in:
parent
4d81b48dd3
commit
4ecf2aaca2
|
|
@ -40,6 +40,12 @@ namespace Content.Client.Input
|
|||
common.AddFunction(ContentKeyFunctions.ResetZoom);
|
||||
common.AddFunction(ContentKeyFunctions.InspectEntity);
|
||||
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
|
||||
// DeltaV - Begin NanoChat keybinds
|
||||
common.AddFunction(ContentKeyFunctions.NanoChatNavigateUp);
|
||||
common.AddFunction(ContentKeyFunctions.NanoChatNavigateDown);
|
||||
common.AddFunction(ContentKeyFunctions.NanoChatNavigateUpUnread);
|
||||
common.AddFunction(ContentKeyFunctions.NanoChatNavigateDownUnread);
|
||||
// DeltaV - End NanoChat keybinds
|
||||
|
||||
// Not in engine, because engine cannot check for sanbox/admin status before starting placement.
|
||||
common.AddFunction(ContentKeyFunctions.EditorCopyObject);
|
||||
|
|
|
|||
|
|
@ -240,6 +240,14 @@ namespace Content.Client.Options.UI.Tabs
|
|||
AddButton(EngineKeyFunctions.EscapeMenu);
|
||||
AddButton(ContentKeyFunctions.EscapeContext);
|
||||
|
||||
// DeltaV - Begin NanoChat keybinds
|
||||
AddHeader("ui-options-header-nano-chat");
|
||||
AddButton(ContentKeyFunctions.NanoChatNavigateUp);
|
||||
AddButton(ContentKeyFunctions.NanoChatNavigateDown);
|
||||
AddButton(ContentKeyFunctions.NanoChatNavigateUpUnread);
|
||||
AddButton(ContentKeyFunctions.NanoChatNavigateDownUnread);
|
||||
// DeltaV - End NanoChat keybinds
|
||||
|
||||
// Shitmed Change Start - TODO: Add hands, feet and groin targeting.
|
||||
AddHeader("ui-options-header-targeting");
|
||||
AddButton(ContentKeyFunctions.TargetHead);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc nano-chat-edit-title}"
|
||||
MinSize="300 200">
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<!-- Number input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-number-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="NumberInput"
|
||||
PlaceHolder="{Loc nano-chat-number-placeholder}"
|
||||
Editable="False" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Name input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-name-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="NameInput"
|
||||
PlaceHolder="{Loc nano-chat-name-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Job input (optional) -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-chat-job-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="JobInput"
|
||||
PlaceHolder="{Loc nano-chat-job-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0">
|
||||
<Button Name="CancelButton"
|
||||
Text="{Loc nano-chat-cancel}"
|
||||
StyleClasses="OpenRight"
|
||||
MinSize="80 0" />
|
||||
<Button Name="ConfirmButton"
|
||||
Text="{Loc nano-chat-confirm}"
|
||||
StyleClasses="OpenLeft"
|
||||
MinSize="80 0"
|
||||
Disabled="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using Content.Shared.Access.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class EditChatPopup : DefaultWindow
|
||||
{
|
||||
private const int MaxNumberLength = 4;
|
||||
|
||||
// Used to see if the user input is different from the original data
|
||||
// to check if the user can submit
|
||||
private string _originalNumber = "";
|
||||
private string _originalName = "";
|
||||
private string _originalJob = "";
|
||||
|
||||
public event Action<uint, string, string?>? OnContactEdited;
|
||||
|
||||
public EditChatPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
// margins trolling
|
||||
ContentsContainer.Margin = new Thickness(3);
|
||||
|
||||
// Button handlers
|
||||
CancelButton.OnPressed += _ => Close();
|
||||
ConfirmButton.OnPressed += _ => EditChat();
|
||||
|
||||
NameInput.OnTabComplete += _ => JobInput.GrabKeyboardFocus();
|
||||
NameInput.OnTextEntered += _ => EditChat();
|
||||
|
||||
JobInput.OnTabComplete += _ => NameInput.GrabKeyboardFocus();
|
||||
JobInput.OnTextEntered += _ => EditChat();
|
||||
|
||||
// Input validation
|
||||
NameInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > IdCardConsoleComponent.MaxFullNameLength)
|
||||
NameInput.Text = args.Text[..IdCardConsoleComponent.MaxFullNameLength];
|
||||
ValidateInputs();
|
||||
};
|
||||
|
||||
JobInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > IdCardConsoleComponent.MaxJobTitleLength)
|
||||
JobInput.Text = args.Text[..IdCardConsoleComponent.MaxJobTitleLength];
|
||||
ValidateInputs();
|
||||
};
|
||||
}
|
||||
|
||||
private void ValidateInputs()
|
||||
{
|
||||
var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
|
||||
!string.IsNullOrWhiteSpace(NameInput.Text) &&
|
||||
NumberInput.Text.Length == MaxNumberLength &&
|
||||
uint.TryParse(NumberInput.Text, out _) &&
|
||||
// Only valid if there are any changes
|
||||
(NumberInput.Text != _originalNumber ||
|
||||
NameInput.Text != _originalName ||
|
||||
JobInput.Text != _originalJob);
|
||||
|
||||
ConfirmButton.Disabled = !isValid;
|
||||
}
|
||||
|
||||
private void EditChat()
|
||||
{
|
||||
if (!uint.TryParse(NumberInput.Text, out var number))
|
||||
return;
|
||||
|
||||
var name = NameInput.Text.Trim();
|
||||
var job = string.IsNullOrWhiteSpace(JobInput.Text) ? null : JobInput.Text.Trim();
|
||||
|
||||
OnContactEdited?.Invoke(number, name, job);
|
||||
Close();
|
||||
}
|
||||
|
||||
public void ClearInputs()
|
||||
{
|
||||
NameInput.Text = string.Empty;
|
||||
JobInput.Text = string.Empty;
|
||||
ValidateInputs();
|
||||
}
|
||||
|
||||
public void SetNumberInput(string newNumber)
|
||||
{
|
||||
NumberInput.Text = newNumber.PadLeft(MaxNumberLength, '0');
|
||||
_originalNumber = newNumber;
|
||||
}
|
||||
|
||||
public void SetNameInput(string newName)
|
||||
{
|
||||
NameInput.Text = newName;
|
||||
_originalName = newName;
|
||||
}
|
||||
|
||||
public void SetJobInput(string newJob)
|
||||
{
|
||||
JobInput.Text = newJob;
|
||||
_originalJob = newJob;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||
using Content.Shared._DV.NanoChat;
|
||||
using Content.Shared.Access.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
|
@ -29,8 +31,8 @@ public sealed partial class NanoChatEntry : BoxContainer
|
|||
_pressHandler = _ => OnPressed?.Invoke(_number);
|
||||
ChatButton.OnPressed += _pressHandler;
|
||||
|
||||
NameLabel.Text = recipient.Name;
|
||||
JobLabel.Text = recipient.JobTitle ?? "";
|
||||
NameLabel.Text = SharedNanoChatSystem.Truncate(recipient.Name, IdCardConsoleComponent.MaxFullNameLength);
|
||||
JobLabel.Text = SharedNanoChatSystem.Truncate(recipient.JobTitle ?? "", IdCardConsoleComponent.MaxJobTitleLength);
|
||||
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
|
||||
UnreadIndicator.Visible = recipient.HasUnread;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using Content.Shared._DV.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
|
|
@ -28,6 +29,19 @@ public sealed partial class NanoChatLookupView : PanelContainer
|
|||
for (var idx = 0; idx < contacts.Count; idx++)
|
||||
{
|
||||
var contact = contacts[idx];
|
||||
var isEvenRow = idx % 2 == 0;
|
||||
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
|
||||
ContactsList.AddChild(contactControl);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ContactContainer : PanelContainer
|
||||
{
|
||||
public ContactContainer(NanoChatRecipient contact, NanoChatUiState state, bool isEvenRow, Action<NanoChatRecipient>? onStartChat)
|
||||
{
|
||||
HorizontalExpand = true;
|
||||
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
|
||||
|
||||
var nameLabel = new Label()
|
||||
{
|
||||
Text = contact.Name,
|
||||
|
|
@ -36,7 +50,7 @@ public sealed partial class NanoChatLookupView : PanelContainer
|
|||
};
|
||||
var numberLabel = new Label()
|
||||
{
|
||||
Text = $"#{contacts[idx].Number:D4}",
|
||||
Text = $"#{contact.Number:D4}",
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
Margin = new Thickness(0, 0, 36, 0),
|
||||
};
|
||||
|
|
@ -49,25 +63,17 @@ public sealed partial class NanoChatLookupView : PanelContainer
|
|||
ToolTip = Loc.GetString("nano-chat-new-chat"),
|
||||
};
|
||||
startChatButton.AddStyleClass("OpenBoth");
|
||||
|
||||
if (contact.Number == state.OwnNumber || state.Recipients.ContainsKey(contact.Number) || state.MaxRecipients <= state.Recipients.Count)
|
||||
{
|
||||
startChatButton.Disabled = true;
|
||||
}
|
||||
startChatButton.OnPressed += _ => OnStartChat?.Invoke(contact);
|
||||
|
||||
var panel = new PanelContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
startChatButton.OnPressed += _ => onStartChat?.Invoke(contact);
|
||||
|
||||
panel.AddChild(nameLabel);
|
||||
panel.AddChild(numberLabel);
|
||||
panel.AddChild(startChatButton);
|
||||
|
||||
var styleClass = idx % 2 == 0 ? "PanelBackgroundBaseDark" : "PanelBackgroundLight";
|
||||
panel.StyleClasses.Add(styleClass);
|
||||
|
||||
ContactsList.AddChild(panel);
|
||||
AddChild(nameLabel);
|
||||
AddChild(numberLabel);
|
||||
AddChild(startChatButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,17 +34,6 @@
|
|||
StyleClasses="LabelSubText"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 8 0" />
|
||||
<Button Name="DeleteChatButton"
|
||||
MaxSize="32 32"
|
||||
Visible="False"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-delete}">
|
||||
<TextureRect StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
</Button>
|
||||
<Button Name="MuteButton"
|
||||
MaxSize="32 32"
|
||||
StyleClasses="OpenBoth"
|
||||
|
|
@ -155,18 +144,59 @@
|
|||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Character count -->
|
||||
<Label Name="CharacterCount"
|
||||
HorizontalAlignment="Center"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 0 4 2"
|
||||
Visible="False" />
|
||||
<!-- Message input -->
|
||||
<BoxContainer Name="MessageInputContainer"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 4 0 0"
|
||||
Visible="False">
|
||||
<!-- Character count -->
|
||||
<Label Name="CharacterCount"
|
||||
HorizontalAlignment="Right"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 0 4 2"
|
||||
Visible="False" />
|
||||
<Button Name="EditChatButton"
|
||||
MaxSize="32 32"
|
||||
Visible="False"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-edit}">
|
||||
<TextureRect StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/_DV/Interface/VerbIcons/edit.svg.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
</Button>
|
||||
<Button Name="DeleteChatButton"
|
||||
MaxSize="32 32"
|
||||
Visible="False"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-delete}">
|
||||
<TextureRect StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
</Button>
|
||||
<Button Name="MuteChatButton"
|
||||
MaxSize="32 32"
|
||||
StyleClasses="OpenBoth"
|
||||
Margin="0 0 4 0"
|
||||
ToolTip="{Loc nano-chat-toggle-mute-chat}">
|
||||
<Control HorizontalExpand="True" VerticalExpand="True">
|
||||
<TextureRect Name="BellIconContact"
|
||||
StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/_DV/Interface/VerbIcons/bell.svg.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
MinSize="18 18" />
|
||||
<TextureRect Name="BellMutedIconContact"
|
||||
StyleClasses="ButtonSquare"
|
||||
TexturePath="/Textures/_DV/Interface/VerbIcons/bell_muted.png"
|
||||
Stretch="KeepAspectCentered"
|
||||
Visible="False"
|
||||
MinSize="18 18" />
|
||||
</Control>
|
||||
</Button>
|
||||
<!-- Input row -->
|
||||
<LineEdit Name="MessageInput"
|
||||
PlaceHolder="{Loc nano-chat-message-placeholder}"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Robust.Client.UserInterface.Controls;
|
|||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared.Input;
|
||||
|
||||
namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
||||
|
||||
|
|
@ -14,16 +15,16 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private const int MaxMessageLength = 256;
|
||||
|
||||
private readonly NewChatPopup _newChatPopup;
|
||||
private readonly EditChatPopup _editChatPopup;
|
||||
private uint? _currentChat;
|
||||
private uint? _pendingChat;
|
||||
private uint _ownNumber;
|
||||
private bool _notificationsMuted;
|
||||
private bool _listNumber = true;
|
||||
private Dictionary<uint, NanoChatRecipient> _recipients = new();
|
||||
private Dictionary<uint, List<NanoChatMessage>> _messages = new();
|
||||
private Dictionary<uint, NanoChatRecipient> _recipients = [];
|
||||
private Dictionary<uint, List<NanoChatMessage>> _messages = [];
|
||||
private HashSet<uint> _mutedChats = [];
|
||||
|
||||
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
|
||||
|
||||
|
|
@ -32,7 +33,8 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_newChatPopup = new NewChatPopup();
|
||||
_newChatPopup = new();
|
||||
_editChatPopup = new();
|
||||
SetupEventHandlers();
|
||||
}
|
||||
|
||||
|
|
@ -43,12 +45,30 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job);
|
||||
};
|
||||
|
||||
_editChatPopup.OnContactEdited += (number, name, job) =>
|
||||
{
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
|
||||
};
|
||||
|
||||
NewChatButton.OnPressed += _ =>
|
||||
{
|
||||
_newChatPopup.ClearInputs();
|
||||
_newChatPopup.OpenCentered();
|
||||
};
|
||||
|
||||
MuteChatButton.OnPressed += _ =>
|
||||
{
|
||||
if (_currentChat is not uint currentChat)
|
||||
return;
|
||||
|
||||
// Remove if muted, otherwise add
|
||||
if (!_mutedChats.Remove(currentChat))
|
||||
_mutedChats.Add(currentChat);
|
||||
|
||||
UpdateMuteChatButton();
|
||||
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMuteChat, currentChat, null, null);
|
||||
};
|
||||
|
||||
MuteButton.OnPressed += _ =>
|
||||
{
|
||||
_notificationsMuted = !_notificationsMuted;
|
||||
|
|
@ -56,22 +76,33 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null);
|
||||
};
|
||||
|
||||
MessageInput.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function == ContentKeyFunctions.NanoChatNavigateUpUnread)
|
||||
CycleChannel(CycleDirection.Up, true);
|
||||
else if (args.Function == ContentKeyFunctions.NanoChatNavigateDownUnread)
|
||||
CycleChannel(CycleDirection.Down, true);
|
||||
else if (args.Function == ContentKeyFunctions.NanoChatNavigateUp)
|
||||
CycleChannel(CycleDirection.Up, false);
|
||||
else if (args.Function == ContentKeyFunctions.NanoChatNavigateDown)
|
||||
CycleChannel(CycleDirection.Down, false);
|
||||
};
|
||||
MessageInput.OnTextChanged += args =>
|
||||
{
|
||||
var length = args.Text.Length;
|
||||
var isValid = !string.IsNullOrWhiteSpace(args.Text) &&
|
||||
length <= MaxMessageLength &&
|
||||
length <= NanoChatMessage.MaxContentLength &&
|
||||
(_currentChat != null || _pendingChat != null);
|
||||
|
||||
SendButton.Disabled = !isValid;
|
||||
|
||||
// Show character count when over limit
|
||||
CharacterCount.Visible = length > MaxMessageLength;
|
||||
if (length > MaxMessageLength)
|
||||
CharacterCount.Visible = length > NanoChatMessage.MaxContentLength;
|
||||
if (length > NanoChatMessage.MaxContentLength)
|
||||
{
|
||||
CharacterCount.Text = Loc.GetString("nano-chat-message-too-long",
|
||||
("current", length),
|
||||
("max", MaxMessageLength));
|
||||
("max", NanoChatMessage.MaxContentLength));
|
||||
CharacterCount.StyleClasses.Add("LabelDanger");
|
||||
}
|
||||
};
|
||||
|
|
@ -93,7 +124,9 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleListNumber, null, null, null);
|
||||
};
|
||||
|
||||
MessageInput.OnTextEntered += _ => SendMessage();
|
||||
SendButton.OnPressed += _ => SendMessage();
|
||||
EditChatButton.OnPressed += _ => BeginEditChat();
|
||||
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +137,44 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
LookupButton.Pressed = LookupView.Visible;
|
||||
}
|
||||
|
||||
public enum CycleDirection : byte
|
||||
{
|
||||
Up,
|
||||
Down,
|
||||
};
|
||||
|
||||
private void CycleChannel(CycleDirection direction, bool onlyUnread)
|
||||
{
|
||||
if (_recipients.Count == 0)
|
||||
return;
|
||||
|
||||
var orderedRecipients = _recipients.OrderBy(r => r.Value.Name).Select(r => r.Key).ToArray();
|
||||
var currentChatIndex = (direction, _currentChat) switch
|
||||
{
|
||||
(CycleDirection.Up, null) => _recipients.Count,
|
||||
(CycleDirection.Down, null) => 0,
|
||||
(_, uint currentChat) => Array.IndexOf(orderedRecipients, currentChat),
|
||||
_ => 0
|
||||
};
|
||||
var newChatIndex = currentChatIndex;
|
||||
|
||||
do
|
||||
{
|
||||
newChatIndex = direction switch
|
||||
{
|
||||
CycleDirection.Up => newChatIndex - 1,
|
||||
CycleDirection.Down => newChatIndex + 1,
|
||||
_ => currentChatIndex,
|
||||
};
|
||||
if (newChatIndex < 0)
|
||||
newChatIndex = _recipients.Count - 1;
|
||||
else if (newChatIndex >= _recipients.Count)
|
||||
newChatIndex = 0;
|
||||
} while (onlyUnread && newChatIndex != currentChatIndex && !_recipients[orderedRecipients[newChatIndex]].HasUnread);
|
||||
|
||||
SelectChat(orderedRecipients[newChatIndex]);
|
||||
}
|
||||
|
||||
private void SendMessage()
|
||||
{
|
||||
var activeChat = _pendingChat ?? _currentChat;
|
||||
|
|
@ -111,6 +182,12 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
return;
|
||||
|
||||
var messageContent = MessageInput.Text;
|
||||
if (!string.IsNullOrWhiteSpace(messageContent))
|
||||
{
|
||||
messageContent = messageContent.Trim();
|
||||
if (messageContent.Length > NanoChatMessage.MaxContentLength)
|
||||
messageContent = messageContent[..NanoChatMessage.MaxContentLength];
|
||||
}
|
||||
|
||||
// Add predicted message
|
||||
var predictedMessage = new NanoChatMessage(
|
||||
|
|
@ -167,6 +244,20 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
|
||||
}
|
||||
|
||||
private void BeginEditChat()
|
||||
{
|
||||
if (_currentChat is not uint currentChat)
|
||||
return;
|
||||
|
||||
var recipient = _recipients[currentChat];
|
||||
|
||||
_editChatPopup.ClearInputs();
|
||||
_editChatPopup.SetNumberInput(recipient.Number.ToString());
|
||||
_editChatPopup.SetNameInput(recipient.Name);
|
||||
_editChatPopup.SetJobInput(recipient.JobTitle ?? string.Empty);
|
||||
_editChatPopup.OpenCentered();
|
||||
}
|
||||
|
||||
private void UpdateChatList(Dictionary<uint, NanoChatRecipient> recipients)
|
||||
{
|
||||
ChatList.RemoveAllChildren();
|
||||
|
|
@ -200,6 +291,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
CurrentChatName.Visible = !hasActiveChat;
|
||||
MessageInputContainer.Visible = hasActiveChat;
|
||||
DeleteChatButton.Visible = hasActiveChat;
|
||||
EditChatButton.Visible = hasActiveChat;
|
||||
DeleteChatButton.Disabled = !hasActiveChat;
|
||||
|
||||
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
|
||||
|
|
@ -245,6 +337,12 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
BellMutedIcon.Visible = _notificationsMuted;
|
||||
}
|
||||
|
||||
private void UpdateMuteChatButton()
|
||||
{
|
||||
if (BellMutedIconContact != null)
|
||||
BellMutedIconContact.Visible = _currentChat is uint currentChat && _mutedChats.Contains(currentChat);
|
||||
}
|
||||
|
||||
private void UpdateListNumber()
|
||||
{
|
||||
if (ListNumberButton != null)
|
||||
|
|
@ -256,6 +354,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
_ownNumber = state.OwnNumber;
|
||||
_notificationsMuted = state.NotificationsMuted;
|
||||
_listNumber = state.ListNumber;
|
||||
_mutedChats = state.MutedChats;
|
||||
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
|
||||
UpdateMuteButton();
|
||||
UpdateListNumber();
|
||||
|
|
@ -281,6 +380,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
|
|||
_currentChat = state.CurrentChat;
|
||||
|
||||
UpdateCurrentChat();
|
||||
UpdateMuteChatButton();
|
||||
UpdateChatList(state.Recipients);
|
||||
UpdateMessages(state.Messages);
|
||||
LookupView.UpdateContactList(state);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Access.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
|
@ -8,7 +9,6 @@ namespace Content.Client._DV.CartridgeLoader.Cartridges;
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NewChatPopup : DefaultWindow
|
||||
{
|
||||
private const int MaxInputLength = 16;
|
||||
private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer
|
||||
|
||||
public event Action<uint, string, string?>? OnChatCreated;
|
||||
|
|
@ -24,9 +24,14 @@ public sealed partial class NewChatPopup : DefaultWindow
|
|||
CancelButton.OnPressed += _ => Close();
|
||||
CreateButton.OnPressed += _ => CreateChat();
|
||||
|
||||
// Input validation
|
||||
NumberInput.OnTextChanged += _ => ValidateInputs();
|
||||
NameInput.OnTextChanged += _ => ValidateInputs();
|
||||
NumberInput.OnTabComplete += _ => NameInput.GrabKeyboardFocus();
|
||||
NumberInput.OnTextEntered += _ => CreateChat();
|
||||
|
||||
NameInput.OnTabComplete += _ => JobInput.GrabKeyboardFocus();
|
||||
NameInput.OnTextEntered += _ => CreateChat();
|
||||
|
||||
JobInput.OnTabComplete += _ => NumberInput.GrabKeyboardFocus();
|
||||
JobInput.OnTextEntered += _ => CreateChat();
|
||||
|
||||
// Input validation
|
||||
NumberInput.OnTextChanged += args =>
|
||||
|
|
@ -44,15 +49,15 @@ public sealed partial class NewChatPopup : DefaultWindow
|
|||
|
||||
NameInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > MaxInputLength)
|
||||
NameInput.Text = args.Text[..MaxInputLength];
|
||||
if (args.Text.Length > IdCardConsoleComponent.MaxFullNameLength)
|
||||
NameInput.Text = args.Text[..IdCardConsoleComponent.MaxFullNameLength];
|
||||
ValidateInputs();
|
||||
};
|
||||
|
||||
JobInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > MaxInputLength)
|
||||
JobInput.Text = args.Text[..MaxInputLength];
|
||||
if (args.Text.Length > IdCardConsoleComponent.MaxJobTitleLength)
|
||||
JobInput.Text = args.Text[..IdCardConsoleComponent.MaxJobTitleLength];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +65,7 @@ public sealed partial class NewChatPopup : DefaultWindow
|
|||
{
|
||||
var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
|
||||
!string.IsNullOrWhiteSpace(NameInput.Text) &&
|
||||
NumberInput.Text.Length == MaxNumberLength &&
|
||||
uint.TryParse(NumberInput.Text, out _);
|
||||
|
||||
CreateButton.Disabled = !isValid;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
// no point in storing it on the comp
|
||||
private const int NotificationMaxLength = 64;
|
||||
|
||||
// The max length of the name and job title on the notification before being truncated.
|
||||
private const int NotificationTitleMaxLength = 32;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
|
@ -105,12 +108,18 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
case NanoChatUiMessageType.SelectChat:
|
||||
HandleSelectChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.EditChat:
|
||||
HandleEditChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.CloseChat:
|
||||
HandleCloseChat(card);
|
||||
break;
|
||||
case NanoChatUiMessageType.ToggleMute:
|
||||
HandleToggleMute(card);
|
||||
break;
|
||||
case NanoChatUiMessageType.ToggleMuteChat:
|
||||
HandleToggleMuteChat(card, msg);
|
||||
break;
|
||||
case NanoChatUiMessageType.DeleteChat:
|
||||
HandleDeleteChat(card, msg);
|
||||
break;
|
||||
|
|
@ -155,17 +164,33 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number)
|
||||
return;
|
||||
|
||||
var name = msg.Content;
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = name.Trim();
|
||||
if (name.Length > IdCardConsoleComponent.MaxFullNameLength)
|
||||
name = name[..IdCardConsoleComponent.MaxFullNameLength];
|
||||
}
|
||||
|
||||
var jobTitle = msg.RecipientJob;
|
||||
if (!string.IsNullOrWhiteSpace(jobTitle))
|
||||
{
|
||||
jobTitle = jobTitle.Trim();
|
||||
if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength)
|
||||
jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength];
|
||||
}
|
||||
|
||||
// Add new recipient
|
||||
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
|
||||
msg.Content,
|
||||
msg.RecipientJob);
|
||||
name,
|
||||
jobTitle);
|
||||
|
||||
// Initialize or update recipient
|
||||
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
|
||||
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({msg.Content})");
|
||||
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
|
||||
|
||||
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||
RaiseLocalEvent(ref recipientEv);
|
||||
|
|
@ -191,6 +216,42 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles editing the current chat conversation.
|
||||
/// </summary>
|
||||
private void HandleEditChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number ||
|
||||
_nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value) is not { } recipient)
|
||||
return;
|
||||
|
||||
var name = msg.Content;
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = name.Trim();
|
||||
if (name.Length > IdCardConsoleComponent.MaxFullNameLength)
|
||||
name = name[..IdCardConsoleComponent.MaxFullNameLength];
|
||||
}
|
||||
|
||||
var jobTitle = msg.RecipientJob;
|
||||
if (!string.IsNullOrWhiteSpace(jobTitle))
|
||||
{
|
||||
jobTitle = jobTitle.Trim();
|
||||
if (jobTitle.Length > IdCardConsoleComponent.MaxJobTitleLength)
|
||||
jobTitle = jobTitle[..IdCardConsoleComponent.MaxJobTitleLength];
|
||||
}
|
||||
|
||||
// Update recipient
|
||||
recipient.Name = name;
|
||||
recipient.JobTitle = jobTitle;
|
||||
|
||||
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
|
||||
|
||||
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
|
||||
RaiseLocalEvent(ref recipientEv);
|
||||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles closing the current chat conversation.
|
||||
/// </summary>
|
||||
|
|
@ -229,6 +290,14 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
private void HandleToggleMuteChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
|
||||
{
|
||||
if (msg.RecipientNumber is not uint chat)
|
||||
return;
|
||||
_nanoChat.ToggleChatMuted((card, card.Comp), chat);
|
||||
UpdateUIForCard(card);
|
||||
}
|
||||
|
||||
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card)
|
||||
{
|
||||
_nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp)));
|
||||
|
|
@ -248,10 +317,18 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
|
||||
return;
|
||||
|
||||
var content = msg.Content;
|
||||
if (!string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
content = content.Trim();
|
||||
if (content.Length > NanoChatMessage.MaxContentLength)
|
||||
content = content[..NanoChatMessage.MaxContentLength];
|
||||
}
|
||||
|
||||
// Create and store message for sender
|
||||
var message = new NanoChatMessage(
|
||||
_timing.CurTime,
|
||||
msg.Content,
|
||||
content,
|
||||
(uint)card.Comp.Number
|
||||
);
|
||||
|
||||
|
|
@ -271,7 +348,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
|
||||
_adminLogger.Add(LogType.Chat,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {msg.Content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
|
||||
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
|
||||
|
||||
var msgEv = new NanoChatMessageReceivedEvent(card);
|
||||
RaiseLocalEvent(ref msgEv);
|
||||
|
|
@ -398,18 +475,17 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
Entity<NanoChatCardComponent> recipient,
|
||||
NanoChatMessage message)
|
||||
{
|
||||
var senderNumber = sender.Comp.Number;
|
||||
if (senderNumber == null)
|
||||
if (sender.Comp.Number is not uint senderNumber)
|
||||
return;
|
||||
|
||||
// Always try to get and add sender info to recipient's contacts
|
||||
if (!EnsureRecipientExists(recipient, senderNumber.Value))
|
||||
if (!EnsureRecipientExists(recipient, senderNumber))
|
||||
return;
|
||||
|
||||
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber.Value, message with { DeliveryFailed = false });
|
||||
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber, message with { DeliveryFailed = false });
|
||||
|
||||
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
|
||||
HandleUnreadNotification(recipient, message);
|
||||
HandleUnreadNotification(recipient, message, senderNumber);
|
||||
|
||||
var msgEv = new NanoChatMessageReceivedEvent(recipient);
|
||||
RaiseLocalEvent(ref msgEv);
|
||||
|
|
@ -419,33 +495,49 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
/// <summary>
|
||||
/// Handles unread message notifications and updates unread status.
|
||||
/// </summary>
|
||||
private void HandleUnreadNotification(Entity<NanoChatCardComponent> recipient, NanoChatMessage message)
|
||||
private void HandleUnreadNotification(Entity<NanoChatCardComponent> recipient,
|
||||
NanoChatMessage message,
|
||||
uint senderNumber)
|
||||
{
|
||||
// Get sender name from contacts or fall back to number
|
||||
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
|
||||
var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient)
|
||||
? existingRecipient.Name
|
||||
var senderName = recipients.TryGetValue(message.SenderId, out var senderRecipient)
|
||||
? senderRecipient.Name
|
||||
: $"#{message.SenderId:D4}";
|
||||
|
||||
if (!recipient.Comp.Recipients[message.SenderId].HasUnread && !recipient.Comp.NotificationsMuted)
|
||||
{
|
||||
var pdaQuery = EntityQueryEnumerator<PdaComponent>();
|
||||
while (pdaQuery.MoveNext(out var pdaUid, out var pdaComp))
|
||||
{
|
||||
if (pdaComp.ContainedId != recipient)
|
||||
continue;
|
||||
|
||||
_cartridge.SendNotification(pdaUid,
|
||||
Loc.GetString("nano-chat-new-message-title", ("sender", senderName)),
|
||||
Loc.GetString("nano-chat-new-message-body", ("message", TruncateMessage(message.Content))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
|
||||
|
||||
// Update unread status
|
||||
_nanoChat.SetRecipient((recipient, recipient.Comp),
|
||||
message.SenderId,
|
||||
existingRecipient with { HasUnread = true });
|
||||
if (!hasSelectedCurrentChat)
|
||||
_nanoChat.SetRecipient((recipient, recipient.Comp),
|
||||
message.SenderId,
|
||||
senderRecipient with { HasUnread = true });
|
||||
|
||||
// Temporary local to avoid trouble with read-only access; Contains doesn't modify the collection
|
||||
HashSet<uint> mutedChats = recipient.Comp.MutedChats;
|
||||
if (recipient.Comp.NotificationsMuted ||
|
||||
mutedChats.Contains(message.SenderId) ||
|
||||
recipient.Comp.PdaUid is not { } pdaUid ||
|
||||
!TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) ||
|
||||
// Don't notify if the recipient has the NanoChat program open with this chat selected.
|
||||
(hasSelectedCurrentChat &&
|
||||
_ui.IsUiOpen(pdaUid, PdaUiKey.Key) &&
|
||||
HasComp<NanoChatCartridgeComponent>(loader.ActiveProgram)))
|
||||
return;
|
||||
|
||||
var title = "";
|
||||
if (!string.IsNullOrEmpty(senderRecipient.JobTitle))
|
||||
{
|
||||
var titleRecipient = SharedNanoChatSystem.Truncate(Loc.GetString("nano-chat-new-message-title-recipient",
|
||||
("sender", senderName), ("jobTitle", senderRecipient.JobTitle)), NotificationTitleMaxLength, " \\[...\\]");
|
||||
title = Loc.GetString("nano-chat-new-message-title", ("sender", titleRecipient));
|
||||
}
|
||||
else
|
||||
title = Loc.GetString("nano-chat-new-message-title", ("sender", senderName));
|
||||
|
||||
_cartridge.SendNotification(pdaUid,
|
||||
title,
|
||||
Loc.GetString("nano-chat-new-message-body", ("message", SharedNanoChatSystem.Truncate(message.Content, NotificationMaxLength, " [...]"))),
|
||||
loader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -505,16 +597,6 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a message to the notification maximum length.
|
||||
/// </summary>
|
||||
private static string TruncateMessage(string message)
|
||||
{
|
||||
return message.Length <= NotificationMaxLength
|
||||
? message
|
||||
: message[..(NotificationMaxLength - 4)] + " [...]";
|
||||
}
|
||||
|
||||
private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
|
||||
{
|
||||
_cartridge.RegisterBackgroundProgram(args.Loader, ent);
|
||||
|
|
@ -547,6 +629,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
|
||||
var recipients = new Dictionary<uint, NanoChatRecipient>();
|
||||
var messages = new Dictionary<uint, List<NanoChatMessage>>();
|
||||
var mutedChats = new HashSet<uint>();
|
||||
uint? currentChat = null;
|
||||
uint ownNumber = 0;
|
||||
var maxRecipients = 50;
|
||||
|
|
@ -557,6 +640,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
{
|
||||
recipients = card.Recipients;
|
||||
messages = card.Messages;
|
||||
mutedChats = card.MutedChats;
|
||||
currentChat = card.CurrentChat;
|
||||
ownNumber = card.Number ?? 0;
|
||||
maxRecipients = card.MaxRecipients;
|
||||
|
|
@ -566,6 +650,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
|
|||
|
||||
var state = new NanoChatUiState(recipients,
|
||||
messages,
|
||||
mutedChats,
|
||||
contacts,
|
||||
currentChat,
|
||||
ownNumber,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ using Content.Shared._DV.CartridgeLoader.Cartridges;
|
|||
using Content.Shared._DV.NanoChat;
|
||||
using Content.Shared.Kitchen.Components;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.PDA;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
|
|
@ -26,10 +28,32 @@ public sealed class NanoChatSystem : SharedNanoChatSystem
|
|||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NanoChatCardComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
|
||||
SubscribeLocalEvent<NanoChatCardComponent, EntGotRemovedFromContainerMessage>(OnRemoved);
|
||||
|
||||
SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit);
|
||||
SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]);
|
||||
}
|
||||
|
||||
private void OnInserted(Entity<NanoChatCardComponent> ent, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != PdaComponent.PdaIdSlotId)
|
||||
return;
|
||||
|
||||
ent.Comp.PdaUid = args.Container.Owner;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnRemoved(Entity<NanoChatCardComponent> ent, ref EntGotRemovedFromContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != PdaComponent.PdaIdSlotId)
|
||||
return;
|
||||
|
||||
ent.Comp.PdaUid = null;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args)
|
||||
{
|
||||
// Skip if the entity was deleted (e.g., by ID card system burning it)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ namespace Content.Shared.Input
|
|||
public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward";
|
||||
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
|
||||
public static readonly BoundKeyFunction EscapeContext = "EscapeContext";
|
||||
// DeltaV - Begin NanoChat keybinds
|
||||
public static readonly BoundKeyFunction NanoChatNavigateUp = "NanoChatNavigateUp";
|
||||
public static readonly BoundKeyFunction NanoChatNavigateDown = "NanoChatNavigateDown";
|
||||
public static readonly BoundKeyFunction NanoChatNavigateUpUnread = "NanoChatNavigateUpUnread";
|
||||
public static readonly BoundKeyFunction NanoChatNavigateDownUnread = "NanoChatNavigateDownUnread";
|
||||
// DeltaV - End NanoChat keybinds
|
||||
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
|
||||
public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu";
|
||||
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
|
||||
|
|
|
|||
|
|
@ -50,10 +50,12 @@ public enum NanoChatUiMessageType : byte
|
|||
{
|
||||
NewChat,
|
||||
SelectChat,
|
||||
EditChat,
|
||||
CloseChat,
|
||||
SendMessage,
|
||||
DeleteChat,
|
||||
ToggleMute,
|
||||
ToggleMuteChat,
|
||||
ToggleListNumber,
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +102,8 @@ public struct NanoChatRecipient
|
|||
[Serializable, NetSerializable, DataRecord]
|
||||
public struct NanoChatMessage
|
||||
{
|
||||
public const int MaxContentLength = 256;
|
||||
|
||||
/// <summary>
|
||||
/// When the message was sent.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ namespace Content.Shared._DV.CartridgeLoader.Cartridges;
|
|||
[Serializable, NetSerializable]
|
||||
public sealed class NanoChatUiState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly Dictionary<uint, NanoChatRecipient> Recipients = new();
|
||||
public readonly Dictionary<uint, List<NanoChatMessage>> Messages = new();
|
||||
public readonly Dictionary<uint, NanoChatRecipient> Recipients = [];
|
||||
public readonly Dictionary<uint, List<NanoChatMessage>> Messages = [];
|
||||
public readonly HashSet<uint> MutedChats = [];
|
||||
public readonly List<NanoChatRecipient>? Contacts;
|
||||
public readonly uint? CurrentChat;
|
||||
public readonly uint OwnNumber;
|
||||
|
|
@ -17,6 +18,7 @@ public sealed class NanoChatUiState : BoundUserInterfaceState
|
|||
public NanoChatUiState(
|
||||
Dictionary<uint, NanoChatRecipient> recipients,
|
||||
Dictionary<uint, List<NanoChatMessage>> messages,
|
||||
HashSet<uint> mutedChats,
|
||||
List<NanoChatRecipient>? contacts,
|
||||
uint? currentChat,
|
||||
uint ownNumber,
|
||||
|
|
@ -26,6 +28,7 @@ public sealed class NanoChatUiState : BoundUserInterfaceState
|
|||
{
|
||||
Recipients = recipients;
|
||||
Messages = messages;
|
||||
MutedChats = mutedChats;
|
||||
Contacts = contacts;
|
||||
CurrentChat = currentChat;
|
||||
OwnNumber = ownNumber;
|
||||
|
|
|
|||
|
|
@ -25,13 +25,19 @@ public sealed partial class NanoChatCardComponent : Component
|
|||
/// All chat recipients stored on this card.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<uint, NanoChatRecipient> Recipients = new();
|
||||
public Dictionary<uint, NanoChatRecipient> Recipients = [];
|
||||
|
||||
/// <summary>
|
||||
/// All messages stored on this card, keyed by recipient number.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<uint, List<NanoChatMessage>> Messages = new();
|
||||
public Dictionary<uint, List<NanoChatMessage>> Messages = [];
|
||||
|
||||
/// <summary>
|
||||
/// The NanoChat numbers that should not give a notification, even when notifications are enabled.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<uint> MutedChats = [];
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected chat recipient number.
|
||||
|
|
@ -62,4 +68,10 @@ public sealed partial class NanoChatCardComponent : Component
|
|||
/// </summary>
|
||||
[DataField]
|
||||
public bool ListNumber = true;
|
||||
|
||||
/// <summary>
|
||||
/// The PDA that this card is currently inserted to.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? PdaUid = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ public abstract class SharedNanoChatSystem : EntitySystem
|
|||
args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper Method for truncating a string to maximum length
|
||||
/// </summary>
|
||||
public static string Truncate(string text, int maxLength, string overflowText = "...")
|
||||
{
|
||||
return text.Length > maxLength
|
||||
? text[..(maxLength - overflowText.Length)] + overflowText
|
||||
: text;
|
||||
}
|
||||
|
||||
#region Public API Methods
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -188,6 +198,19 @@ public abstract class SharedNanoChatSystem : EntitySystem
|
|||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether notifications are muted for a specific chat.
|
||||
/// </summary>
|
||||
public void ToggleChatMuted(Entity<NanoChatCardComponent?> card, uint chat)
|
||||
{
|
||||
if (!Resolve(card, ref card.Comp))
|
||||
return;
|
||||
|
||||
if (!card.Comp.MutedChats.Remove(chat))
|
||||
card.Comp.MutedChats.Add(chat);
|
||||
Dirty(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether NanoChat number is listed.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -170,13 +170,16 @@ nano-chat-no-chats = No active chats
|
|||
nano-chat-select-chat = Select a chat to begin
|
||||
nano-chat-message-placeholder = Type a message...
|
||||
nano-chat-send = Send
|
||||
nano-chat-edit = Edit Contact
|
||||
nano-chat-delete = Delete
|
||||
nano-chat-loading = Loading...
|
||||
nano-chat-message-too-long = Message too long ({$current}/{$max} characters)
|
||||
nano-chat-max-recipients = Maximum number of chats reached
|
||||
nano-chat-new-message-title = Message from {$sender}
|
||||
nano-chat-new-message-title-recipient = {$sender} ({$jobTitle})
|
||||
nano-chat-new-message-body = {$message}
|
||||
nano-chat-toggle-mute = Mute notifications
|
||||
nano-chat-toggle-mute-chat = Mute chat
|
||||
nano-chat-delivery-failed = Failed to deliver
|
||||
nano-chat-look-up-no-server = No valid telecommunications server found
|
||||
nano-chat-look-up = Look up numbers
|
||||
|
|
@ -193,6 +196,10 @@ nano-chat-job-placeholder = Enter a job title (optional)
|
|||
nano-chat-cancel = Cancel
|
||||
nano-chat-create = Create
|
||||
|
||||
# Edit chat popup
|
||||
nano-chat-edit-title = Edit a contact
|
||||
nano-chat-confirm = Confirm
|
||||
|
||||
# LogProbe additions
|
||||
log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs
|
||||
log-probe-header-access = Access Log Scanner
|
||||
|
|
|
|||
|
|
@ -3,3 +3,10 @@ ui-options-general-forknotice = Note: These settings are fork-specific and might
|
|||
|
||||
ui-options-no-filters = Disable species vision filters
|
||||
ui-options-function-swap-hands-reversed = Swap hands (reversed)
|
||||
|
||||
## DeltaV NanoChat keybinds
|
||||
ui-options-header-nano-chat = NanoChat
|
||||
ui-options-function-nano-chat-navigate-up = Navigate up
|
||||
ui-options-function-nano-chat-navigate-down = Navigate down
|
||||
ui-options-function-nano-chat-navigate-up-unread = Navigate up to next unread
|
||||
ui-options-function-nano-chat-navigate-down-unread = Navigate down to next unread
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@ Licensed under CC BY 4.0
|
|||
|
||||
hamburger_icon.svg.png
|
||||
Released into the public domain
|
||||
|
||||
edit.svg.png by H2D2 Design under MIT License
|
||||
https://www.svgrepo.com/svg/453928/edit
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 924 B |
|
|
@ -165,12 +165,30 @@ binds:
|
|||
- function: SwapHands
|
||||
type: State
|
||||
key: X
|
||||
# DeltaV - Swap Hands Reversed Start
|
||||
# DeltaV - Begin custom keybinds
|
||||
- function: SwapHandsReversed
|
||||
type: State
|
||||
key: X
|
||||
mod1: Shift
|
||||
# DeltaV - Swap Hands Reversed End
|
||||
- function: NanoChatNavigateUp
|
||||
type: State
|
||||
key: Up
|
||||
mod1: Alt
|
||||
- function: NanoChatNavigateDown
|
||||
type: State
|
||||
key: Down
|
||||
mod1: Alt
|
||||
- function: NanoChatNavigateUpUnread
|
||||
type: State
|
||||
key: Up
|
||||
mod1: Alt
|
||||
mod2: Shift
|
||||
- function: NanoChatNavigateDownUnread
|
||||
type: State
|
||||
key: Down
|
||||
mod1: Alt
|
||||
mod2: Shift
|
||||
# DeltaV - End custom keybinds
|
||||
- function: MoveStoredItem
|
||||
type: State
|
||||
key: MouseLeft
|
||||
|
|
|
|||
Loading…
Reference in New Issue