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:
Tobias Berger 2025-02-23 16:59:24 +01:00 committed by GitHub
parent 4d81b48dd3
commit 4ecf2aaca2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 604 additions and 97 deletions

View File

@ -40,6 +40,12 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.ResetZoom); common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity); common.AddFunction(ContentKeyFunctions.InspectEntity);
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow); 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. // Not in engine, because engine cannot check for sanbox/admin status before starting placement.
common.AddFunction(ContentKeyFunctions.EditorCopyObject); common.AddFunction(ContentKeyFunctions.EditorCopyObject);

View File

@ -240,6 +240,14 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.EscapeMenu); AddButton(EngineKeyFunctions.EscapeMenu);
AddButton(ContentKeyFunctions.EscapeContext); 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. // Shitmed Change Start - TODO: Add hands, feet and groin targeting.
AddHeader("ui-options-header-targeting"); AddHeader("ui-options-header-targeting");
AddButton(ContentKeyFunctions.TargetHead); AddButton(ContentKeyFunctions.TargetHead);

View File

@ -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>

View File

@ -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;
}
}

View File

@ -1,4 +1,6 @@
using Content.Shared._DV.CartridgeLoader.Cartridges; using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared._DV.NanoChat;
using Content.Shared.Access.Components;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@ -29,8 +31,8 @@ public sealed partial class NanoChatEntry : BoxContainer
_pressHandler = _ => OnPressed?.Invoke(_number); _pressHandler = _ => OnPressed?.Invoke(_number);
ChatButton.OnPressed += _pressHandler; ChatButton.OnPressed += _pressHandler;
NameLabel.Text = recipient.Name; NameLabel.Text = SharedNanoChatSystem.Truncate(recipient.Name, IdCardConsoleComponent.MaxFullNameLength);
JobLabel.Text = recipient.JobTitle ?? ""; JobLabel.Text = SharedNanoChatSystem.Truncate(recipient.JobTitle ?? "", IdCardConsoleComponent.MaxJobTitleLength);
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle); JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
UnreadIndicator.Visible = recipient.HasUnread; UnreadIndicator.Visible = recipient.HasUnread;

View File

@ -1,6 +1,7 @@
using System.Numerics; using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges; using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@ -28,6 +29,19 @@ public sealed partial class NanoChatLookupView : PanelContainer
for (var idx = 0; idx < contacts.Count; idx++) for (var idx = 0; idx < contacts.Count; idx++)
{ {
var contact = contacts[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() var nameLabel = new Label()
{ {
Text = contact.Name, Text = contact.Name,
@ -36,7 +50,7 @@ public sealed partial class NanoChatLookupView : PanelContainer
}; };
var numberLabel = new Label() var numberLabel = new Label()
{ {
Text = $"#{contacts[idx].Number:D4}", Text = $"#{contact.Number:D4}",
HorizontalAlignment = HAlignment.Right, HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, 36, 0), Margin = new Thickness(0, 0, 36, 0),
}; };
@ -49,25 +63,17 @@ public sealed partial class NanoChatLookupView : PanelContainer
ToolTip = Loc.GetString("nano-chat-new-chat"), ToolTip = Loc.GetString("nano-chat-new-chat"),
}; };
startChatButton.AddStyleClass("OpenBoth"); startChatButton.AddStyleClass("OpenBoth");
if (contact.Number == state.OwnNumber || state.Recipients.ContainsKey(contact.Number) || state.MaxRecipients <= state.Recipients.Count) if (contact.Number == state.OwnNumber || state.Recipients.ContainsKey(contact.Number) || state.MaxRecipients <= state.Recipients.Count)
{ {
startChatButton.Disabled = true; startChatButton.Disabled = true;
} }
startChatButton.OnPressed += _ => OnStartChat?.Invoke(contact);
var panel = new PanelContainer() startChatButton.OnPressed += _ => onStartChat?.Invoke(contact);
{
HorizontalExpand = true,
};
panel.AddChild(nameLabel); AddChild(nameLabel);
panel.AddChild(numberLabel); AddChild(numberLabel);
panel.AddChild(startChatButton); AddChild(startChatButton);
var styleClass = idx % 2 == 0 ? "PanelBackgroundBaseDark" : "PanelBackgroundLight";
panel.StyleClasses.Add(styleClass);
ContactsList.AddChild(panel);
} }
} }
} }

View File

@ -34,17 +34,6 @@
StyleClasses="LabelSubText" StyleClasses="LabelSubText"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="0 0 8 0" /> 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" <Button Name="MuteButton"
MaxSize="32 32" MaxSize="32 32"
StyleClasses="OpenBoth" StyleClasses="OpenBoth"
@ -155,18 +144,59 @@
</ScrollContainer> </ScrollContainer>
</BoxContainer> </BoxContainer>
<!-- Character count -->
<Label Name="CharacterCount"
HorizontalAlignment="Center"
StyleClasses="LabelSubText"
Margin="0 0 4 2"
Visible="False" />
<!-- Message input --> <!-- Message input -->
<BoxContainer Name="MessageInputContainer" <BoxContainer Name="MessageInputContainer"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalExpand="True" HorizontalExpand="True"
Margin="0 4 0 0" Margin="0 4 0 0"
Visible="False"> Visible="False">
<!-- Character count --> <Button Name="EditChatButton"
<Label Name="CharacterCount" MaxSize="32 32"
HorizontalAlignment="Right" Visible="False"
StyleClasses="LabelSubText" StyleClasses="OpenBoth"
Margin="0 0 4 2" Margin="0 0 4 0"
Visible="False" /> 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 --> <!-- Input row -->
<LineEdit Name="MessageInput" <LineEdit Name="MessageInput"
PlaceHolder="{Loc nano-chat-message-placeholder}" PlaceHolder="{Loc nano-chat-message-placeholder}"

View File

@ -6,6 +6,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Content.Shared.Input;
namespace Content.Client._DV.CartridgeLoader.Cartridges; namespace Content.Client._DV.CartridgeLoader.Cartridges;
@ -14,16 +15,16 @@ public sealed partial class NanoChatUiFragment : BoxContainer
{ {
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
private const int MaxMessageLength = 256;
private readonly NewChatPopup _newChatPopup; private readonly NewChatPopup _newChatPopup;
private readonly EditChatPopup _editChatPopup;
private uint? _currentChat; private uint? _currentChat;
private uint? _pendingChat; private uint? _pendingChat;
private uint _ownNumber; private uint _ownNumber;
private bool _notificationsMuted; private bool _notificationsMuted;
private bool _listNumber = true; private bool _listNumber = true;
private Dictionary<uint, NanoChatRecipient> _recipients = new(); private Dictionary<uint, NanoChatRecipient> _recipients = [];
private Dictionary<uint, List<NanoChatMessage>> _messages = new(); private Dictionary<uint, List<NanoChatMessage>> _messages = [];
private HashSet<uint> _mutedChats = [];
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent; public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
@ -32,7 +33,8 @@ public sealed partial class NanoChatUiFragment : BoxContainer
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_newChatPopup = new NewChatPopup(); _newChatPopup = new();
_editChatPopup = new();
SetupEventHandlers(); SetupEventHandlers();
} }
@ -43,12 +45,30 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job);
}; };
_editChatPopup.OnContactEdited += (number, name, job) =>
{
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
};
NewChatButton.OnPressed += _ => NewChatButton.OnPressed += _ =>
{ {
_newChatPopup.ClearInputs(); _newChatPopup.ClearInputs();
_newChatPopup.OpenCentered(); _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 += _ => MuteButton.OnPressed += _ =>
{ {
_notificationsMuted = !_notificationsMuted; _notificationsMuted = !_notificationsMuted;
@ -56,22 +76,33 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null); 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 => MessageInput.OnTextChanged += args =>
{ {
var length = args.Text.Length; var length = args.Text.Length;
var isValid = !string.IsNullOrWhiteSpace(args.Text) && var isValid = !string.IsNullOrWhiteSpace(args.Text) &&
length <= MaxMessageLength && length <= NanoChatMessage.MaxContentLength &&
(_currentChat != null || _pendingChat != null); (_currentChat != null || _pendingChat != null);
SendButton.Disabled = !isValid; SendButton.Disabled = !isValid;
// Show character count when over limit // Show character count when over limit
CharacterCount.Visible = length > MaxMessageLength; CharacterCount.Visible = length > NanoChatMessage.MaxContentLength;
if (length > MaxMessageLength) if (length > NanoChatMessage.MaxContentLength)
{ {
CharacterCount.Text = Loc.GetString("nano-chat-message-too-long", CharacterCount.Text = Loc.GetString("nano-chat-message-too-long",
("current", length), ("current", length),
("max", MaxMessageLength)); ("max", NanoChatMessage.MaxContentLength));
CharacterCount.StyleClasses.Add("LabelDanger"); CharacterCount.StyleClasses.Add("LabelDanger");
} }
}; };
@ -93,7 +124,9 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleListNumber, null, null, null); OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleListNumber, null, null, null);
}; };
MessageInput.OnTextEntered += _ => SendMessage();
SendButton.OnPressed += _ => SendMessage(); SendButton.OnPressed += _ => SendMessage();
EditChatButton.OnPressed += _ => BeginEditChat();
DeleteChatButton.OnPressed += _ => DeleteCurrentChat(); DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
} }
@ -104,6 +137,44 @@ public sealed partial class NanoChatUiFragment : BoxContainer
LookupButton.Pressed = LookupView.Visible; 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() private void SendMessage()
{ {
var activeChat = _pendingChat ?? _currentChat; var activeChat = _pendingChat ?? _currentChat;
@ -111,6 +182,12 @@ public sealed partial class NanoChatUiFragment : BoxContainer
return; return;
var messageContent = MessageInput.Text; var messageContent = MessageInput.Text;
if (!string.IsNullOrWhiteSpace(messageContent))
{
messageContent = messageContent.Trim();
if (messageContent.Length > NanoChatMessage.MaxContentLength)
messageContent = messageContent[..NanoChatMessage.MaxContentLength];
}
// Add predicted message // Add predicted message
var predictedMessage = new NanoChatMessage( var predictedMessage = new NanoChatMessage(
@ -167,6 +244,20 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null); 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) private void UpdateChatList(Dictionary<uint, NanoChatRecipient> recipients)
{ {
ChatList.RemoveAllChildren(); ChatList.RemoveAllChildren();
@ -200,6 +291,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
CurrentChatName.Visible = !hasActiveChat; CurrentChatName.Visible = !hasActiveChat;
MessageInputContainer.Visible = hasActiveChat; MessageInputContainer.Visible = hasActiveChat;
DeleteChatButton.Visible = hasActiveChat; DeleteChatButton.Visible = hasActiveChat;
EditChatButton.Visible = hasActiveChat;
DeleteChatButton.Disabled = !hasActiveChat; DeleteChatButton.Disabled = !hasActiveChat;
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient)) if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
@ -245,6 +337,12 @@ public sealed partial class NanoChatUiFragment : BoxContainer
BellMutedIcon.Visible = _notificationsMuted; BellMutedIcon.Visible = _notificationsMuted;
} }
private void UpdateMuteChatButton()
{
if (BellMutedIconContact != null)
BellMutedIconContact.Visible = _currentChat is uint currentChat && _mutedChats.Contains(currentChat);
}
private void UpdateListNumber() private void UpdateListNumber()
{ {
if (ListNumberButton != null) if (ListNumberButton != null)
@ -256,6 +354,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
_ownNumber = state.OwnNumber; _ownNumber = state.OwnNumber;
_notificationsMuted = state.NotificationsMuted; _notificationsMuted = state.NotificationsMuted;
_listNumber = state.ListNumber; _listNumber = state.ListNumber;
_mutedChats = state.MutedChats;
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}"; OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
UpdateMuteButton(); UpdateMuteButton();
UpdateListNumber(); UpdateListNumber();
@ -281,6 +380,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
_currentChat = state.CurrentChat; _currentChat = state.CurrentChat;
UpdateCurrentChat(); UpdateCurrentChat();
UpdateMuteChatButton();
UpdateChatList(state.Recipients); UpdateChatList(state.Recipients);
UpdateMessages(state.Messages); UpdateMessages(state.Messages);
LookupView.UpdateContactList(state); LookupView.UpdateContactList(state);

View File

@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Content.Shared.Access.Components;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@ -8,7 +9,6 @@ namespace Content.Client._DV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class NewChatPopup : DefaultWindow public sealed partial class NewChatPopup : DefaultWindow
{ {
private const int MaxInputLength = 16;
private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer private const int MaxNumberLength = 4; // i hardcoded it to be 4 so suffer
public event Action<uint, string, string?>? OnChatCreated; public event Action<uint, string, string?>? OnChatCreated;
@ -24,9 +24,14 @@ public sealed partial class NewChatPopup : DefaultWindow
CancelButton.OnPressed += _ => Close(); CancelButton.OnPressed += _ => Close();
CreateButton.OnPressed += _ => CreateChat(); CreateButton.OnPressed += _ => CreateChat();
// Input validation NumberInput.OnTabComplete += _ => NameInput.GrabKeyboardFocus();
NumberInput.OnTextChanged += _ => ValidateInputs(); NumberInput.OnTextEntered += _ => CreateChat();
NameInput.OnTextChanged += _ => ValidateInputs();
NameInput.OnTabComplete += _ => JobInput.GrabKeyboardFocus();
NameInput.OnTextEntered += _ => CreateChat();
JobInput.OnTabComplete += _ => NumberInput.GrabKeyboardFocus();
JobInput.OnTextEntered += _ => CreateChat();
// Input validation // Input validation
NumberInput.OnTextChanged += args => NumberInput.OnTextChanged += args =>
@ -44,15 +49,15 @@ public sealed partial class NewChatPopup : DefaultWindow
NameInput.OnTextChanged += args => NameInput.OnTextChanged += args =>
{ {
if (args.Text.Length > MaxInputLength) if (args.Text.Length > IdCardConsoleComponent.MaxFullNameLength)
NameInput.Text = args.Text[..MaxInputLength]; NameInput.Text = args.Text[..IdCardConsoleComponent.MaxFullNameLength];
ValidateInputs(); ValidateInputs();
}; };
JobInput.OnTextChanged += args => JobInput.OnTextChanged += args =>
{ {
if (args.Text.Length > MaxInputLength) if (args.Text.Length > IdCardConsoleComponent.MaxJobTitleLength)
JobInput.Text = args.Text[..MaxInputLength]; JobInput.Text = args.Text[..IdCardConsoleComponent.MaxJobTitleLength];
}; };
} }
@ -60,6 +65,7 @@ public sealed partial class NewChatPopup : DefaultWindow
{ {
var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) && var isValid = !string.IsNullOrWhiteSpace(NumberInput.Text) &&
!string.IsNullOrWhiteSpace(NameInput.Text) && !string.IsNullOrWhiteSpace(NameInput.Text) &&
NumberInput.Text.Length == MaxNumberLength &&
uint.TryParse(NumberInput.Text, out _); uint.TryParse(NumberInput.Text, out _);
CreateButton.Disabled = !isValid; CreateButton.Disabled = !isValid;

View File

@ -31,6 +31,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
// no point in storing it on the comp // no point in storing it on the comp
private const int NotificationMaxLength = 64; 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@ -105,12 +108,18 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
case NanoChatUiMessageType.SelectChat: case NanoChatUiMessageType.SelectChat:
HandleSelectChat(card, msg); HandleSelectChat(card, msg);
break; break;
case NanoChatUiMessageType.EditChat:
HandleEditChat(card, msg);
break;
case NanoChatUiMessageType.CloseChat: case NanoChatUiMessageType.CloseChat:
HandleCloseChat(card); HandleCloseChat(card);
break; break;
case NanoChatUiMessageType.ToggleMute: case NanoChatUiMessageType.ToggleMute:
HandleToggleMute(card); HandleToggleMute(card);
break; break;
case NanoChatUiMessageType.ToggleMuteChat:
HandleToggleMuteChat(card, msg);
break;
case NanoChatUiMessageType.DeleteChat: case NanoChatUiMessageType.DeleteChat:
HandleDeleteChat(card, msg); HandleDeleteChat(card, msg);
break; break;
@ -155,17 +164,33 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number) if (msg.RecipientNumber == null || msg.Content == null || msg.RecipientNumber == card.Comp.Number)
return; 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 // Add new recipient
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value, var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
msg.Content, name,
msg.RecipientJob); jobTitle);
// Initialize or update recipient // Initialize or update recipient
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient); _nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
_adminLogger.Add(LogType.Action, _adminLogger.Add(LogType.Action,
LogImpact.Low, 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); var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv); 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> /// <summary>
/// Handles closing the current chat conversation. /// Handles closing the current chat conversation.
/// </summary> /// </summary>
@ -229,6 +290,14 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
UpdateUIForCard(card); 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) private void HandleToggleListNumber(Entity<NanoChatCardComponent> card)
{ {
_nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp))); _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)) if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
return; 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 // Create and store message for sender
var message = new NanoChatMessage( var message = new NanoChatMessage(
_timing.CurTime, _timing.CurTime,
msg.Content, content,
(uint)card.Comp.Number (uint)card.Comp.Number
); );
@ -271,7 +348,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
_adminLogger.Add(LogType.Chat, _adminLogger.Add(LogType.Chat,
LogImpact.Low, 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); var msgEv = new NanoChatMessageReceivedEvent(card);
RaiseLocalEvent(ref msgEv); RaiseLocalEvent(ref msgEv);
@ -398,18 +475,17 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
Entity<NanoChatCardComponent> recipient, Entity<NanoChatCardComponent> recipient,
NanoChatMessage message) NanoChatMessage message)
{ {
var senderNumber = sender.Comp.Number; if (sender.Comp.Number is not uint senderNumber)
if (senderNumber == null)
return; return;
// Always try to get and add sender info to recipient's contacts // Always try to get and add sender info to recipient's contacts
if (!EnsureRecipientExists(recipient, senderNumber.Value)) if (!EnsureRecipientExists(recipient, senderNumber))
return; 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) if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
HandleUnreadNotification(recipient, message); HandleUnreadNotification(recipient, message, senderNumber);
var msgEv = new NanoChatMessageReceivedEvent(recipient); var msgEv = new NanoChatMessageReceivedEvent(recipient);
RaiseLocalEvent(ref msgEv); RaiseLocalEvent(ref msgEv);
@ -419,33 +495,49 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
/// <summary> /// <summary>
/// Handles unread message notifications and updates unread status. /// Handles unread message notifications and updates unread status.
/// </summary> /// </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 // Get sender name from contacts or fall back to number
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp)); var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
var senderName = recipients.TryGetValue(message.SenderId, out var existingRecipient) var senderName = recipients.TryGetValue(message.SenderId, out var senderRecipient)
? existingRecipient.Name ? senderRecipient.Name
: $"#{message.SenderId:D4}"; : $"#{message.SenderId:D4}";
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
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;
}
}
// Update unread status // Update unread status
if (!hasSelectedCurrentChat)
_nanoChat.SetRecipient((recipient, recipient.Comp), _nanoChat.SetRecipient((recipient, recipient.Comp),
message.SenderId, message.SenderId,
existingRecipient with { HasUnread = true }); 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> /// <summary>
@ -505,16 +597,6 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
return null; 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) private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{ {
_cartridge.RegisterBackgroundProgram(args.Loader, ent); _cartridge.RegisterBackgroundProgram(args.Loader, ent);
@ -547,6 +629,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var recipients = new Dictionary<uint, NanoChatRecipient>(); var recipients = new Dictionary<uint, NanoChatRecipient>();
var messages = new Dictionary<uint, List<NanoChatMessage>>(); var messages = new Dictionary<uint, List<NanoChatMessage>>();
var mutedChats = new HashSet<uint>();
uint? currentChat = null; uint? currentChat = null;
uint ownNumber = 0; uint ownNumber = 0;
var maxRecipients = 50; var maxRecipients = 50;
@ -557,6 +640,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{ {
recipients = card.Recipients; recipients = card.Recipients;
messages = card.Messages; messages = card.Messages;
mutedChats = card.MutedChats;
currentChat = card.CurrentChat; currentChat = card.CurrentChat;
ownNumber = card.Number ?? 0; ownNumber = card.Number ?? 0;
maxRecipients = card.MaxRecipients; maxRecipients = card.MaxRecipients;
@ -566,6 +650,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var state = new NanoChatUiState(recipients, var state = new NanoChatUiState(recipients,
messages, messages,
mutedChats,
contacts, contacts,
currentChat, currentChat,
ownNumber, ownNumber,

View File

@ -7,6 +7,8 @@ using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared._DV.NanoChat; using Content.Shared._DV.NanoChat;
using Content.Shared.Kitchen.Components; using Content.Shared.Kitchen.Components;
using Content.Shared.NameIdentifier; using Content.Shared.NameIdentifier;
using Content.Shared.PDA;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
@ -26,10 +28,32 @@ public sealed class NanoChatSystem : SharedNanoChatSystem
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<NanoChatCardComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<NanoChatCardComponent, EntGotRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit); SubscribeLocalEvent<NanoChatCardComponent, MapInitEvent>(OnCardInit);
SubscribeLocalEvent<NanoChatCardComponent, BeingMicrowavedEvent>(OnMicrowaved, after: [typeof(IdCardSystem)]); 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) private void OnMicrowaved(Entity<NanoChatCardComponent> ent, ref BeingMicrowavedEvent args)
{ {
// Skip if the entity was deleted (e.g., by ID card system burning it) // Skip if the entity was deleted (e.g., by ID card system burning it)

View File

@ -24,6 +24,12 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward"; public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward";
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward"; public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
public static readonly BoundKeyFunction EscapeContext = "EscapeContext"; 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 OpenCharacterMenu = "OpenCharacterMenu";
public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu"; public static readonly BoundKeyFunction OpenEmotesMenu = "OpenEmotesMenu";
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu"; public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";

View File

@ -50,10 +50,12 @@ public enum NanoChatUiMessageType : byte
{ {
NewChat, NewChat,
SelectChat, SelectChat,
EditChat,
CloseChat, CloseChat,
SendMessage, SendMessage,
DeleteChat, DeleteChat,
ToggleMute, ToggleMute,
ToggleMuteChat,
ToggleListNumber, ToggleListNumber,
} }
@ -100,6 +102,8 @@ public struct NanoChatRecipient
[Serializable, NetSerializable, DataRecord] [Serializable, NetSerializable, DataRecord]
public struct NanoChatMessage public struct NanoChatMessage
{ {
public const int MaxContentLength = 256;
/// <summary> /// <summary>
/// When the message was sent. /// When the message was sent.
/// </summary> /// </summary>

View File

@ -5,8 +5,9 @@ namespace Content.Shared._DV.CartridgeLoader.Cartridges;
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class NanoChatUiState : BoundUserInterfaceState public sealed class NanoChatUiState : BoundUserInterfaceState
{ {
public readonly Dictionary<uint, NanoChatRecipient> Recipients = new(); public readonly Dictionary<uint, NanoChatRecipient> Recipients = [];
public readonly Dictionary<uint, List<NanoChatMessage>> Messages = new(); public readonly Dictionary<uint, List<NanoChatMessage>> Messages = [];
public readonly HashSet<uint> MutedChats = [];
public readonly List<NanoChatRecipient>? Contacts; public readonly List<NanoChatRecipient>? Contacts;
public readonly uint? CurrentChat; public readonly uint? CurrentChat;
public readonly uint OwnNumber; public readonly uint OwnNumber;
@ -17,6 +18,7 @@ public sealed class NanoChatUiState : BoundUserInterfaceState
public NanoChatUiState( public NanoChatUiState(
Dictionary<uint, NanoChatRecipient> recipients, Dictionary<uint, NanoChatRecipient> recipients,
Dictionary<uint, List<NanoChatMessage>> messages, Dictionary<uint, List<NanoChatMessage>> messages,
HashSet<uint> mutedChats,
List<NanoChatRecipient>? contacts, List<NanoChatRecipient>? contacts,
uint? currentChat, uint? currentChat,
uint ownNumber, uint ownNumber,
@ -26,6 +28,7 @@ public sealed class NanoChatUiState : BoundUserInterfaceState
{ {
Recipients = recipients; Recipients = recipients;
Messages = messages; Messages = messages;
MutedChats = mutedChats;
Contacts = contacts; Contacts = contacts;
CurrentChat = currentChat; CurrentChat = currentChat;
OwnNumber = ownNumber; OwnNumber = ownNumber;

View File

@ -25,13 +25,19 @@ public sealed partial class NanoChatCardComponent : Component
/// All chat recipients stored on this card. /// All chat recipients stored on this card.
/// </summary> /// </summary>
[DataField] [DataField]
public Dictionary<uint, NanoChatRecipient> Recipients = new(); public Dictionary<uint, NanoChatRecipient> Recipients = [];
/// <summary> /// <summary>
/// All messages stored on this card, keyed by recipient number. /// All messages stored on this card, keyed by recipient number.
/// </summary> /// </summary>
[DataField] [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> /// <summary>
/// The currently selected chat recipient number. /// The currently selected chat recipient number.
@ -62,4 +68,10 @@ public sealed partial class NanoChatCardComponent : Component
/// </summary> /// </summary>
[DataField] [DataField]
public bool ListNumber = true; public bool ListNumber = true;
/// <summary>
/// The PDA that this card is currently inserted to.
/// </summary>
[DataField]
public EntityUid? PdaUid = null;
} }

View File

@ -31,6 +31,16 @@ public abstract class SharedNanoChatSystem : EntitySystem
args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}"))); 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 #region Public API Methods
/// <summary> /// <summary>
@ -188,6 +198,19 @@ public abstract class SharedNanoChatSystem : EntitySystem
Dirty(card); 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> /// <summary>
/// Gets whether NanoChat number is listed. /// Gets whether NanoChat number is listed.
/// </summary> /// </summary>

View File

@ -170,13 +170,16 @@ nano-chat-no-chats = No active chats
nano-chat-select-chat = Select a chat to begin nano-chat-select-chat = Select a chat to begin
nano-chat-message-placeholder = Type a message... nano-chat-message-placeholder = Type a message...
nano-chat-send = Send nano-chat-send = Send
nano-chat-edit = Edit Contact
nano-chat-delete = Delete nano-chat-delete = Delete
nano-chat-loading = Loading... nano-chat-loading = Loading...
nano-chat-message-too-long = Message too long ({$current}/{$max} characters) nano-chat-message-too-long = Message too long ({$current}/{$max} characters)
nano-chat-max-recipients = Maximum number of chats reached nano-chat-max-recipients = Maximum number of chats reached
nano-chat-new-message-title = Message from {$sender} nano-chat-new-message-title = Message from {$sender}
nano-chat-new-message-title-recipient = {$sender} ({$jobTitle})
nano-chat-new-message-body = {$message} nano-chat-new-message-body = {$message}
nano-chat-toggle-mute = Mute notifications nano-chat-toggle-mute = Mute notifications
nano-chat-toggle-mute-chat = Mute chat
nano-chat-delivery-failed = Failed to deliver nano-chat-delivery-failed = Failed to deliver
nano-chat-look-up-no-server = No valid telecommunications server found nano-chat-look-up-no-server = No valid telecommunications server found
nano-chat-look-up = Look up numbers 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-cancel = Cancel
nano-chat-create = Create nano-chat-create = Create
# Edit chat popup
nano-chat-edit-title = Edit a contact
nano-chat-confirm = Confirm
# LogProbe additions # LogProbe additions
log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs log-probe-scan-nanochat = Scanned {$card}'s NanoChat logs
log-probe-header-access = Access Log Scanner log-probe-header-access = Access Log Scanner

View File

@ -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-no-filters = Disable species vision filters
ui-options-function-swap-hands-reversed = Swap hands (reversed) 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

View File

@ -3,3 +3,6 @@ Licensed under CC BY 4.0
hamburger_icon.svg.png hamburger_icon.svg.png
Released into the public domain 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

View File

@ -165,12 +165,30 @@ binds:
- function: SwapHands - function: SwapHands
type: State type: State
key: X key: X
# DeltaV - Swap Hands Reversed Start # DeltaV - Begin custom keybinds
- function: SwapHandsReversed - function: SwapHandsReversed
type: State type: State
key: X key: X
mod1: Shift 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 - function: MoveStoredItem
type: State type: State
key: MouseLeft key: MouseLeft