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 15:59:24 +00:00
committed by GitHub
co-authored by Alex C
parent 4d81b48dd3
commit 4ecf2aaca2
21 changed files with 604 additions and 97 deletions
@@ -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)