diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 9b274403e3..acd16b0b23 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -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); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 17a52b3c5d..5a8fe55dc9 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -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); diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/EditChatPopup.xaml b/Content.Client/_DV/CartridgeLoader/Cartridges/EditChatPopup.xaml new file mode 100644 index 0000000000..f34eb2ce25 --- /dev/null +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/EditChatPopup.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + _recipients = new(); - private Dictionary> _messages = new(); + private Dictionary _recipients = []; + private Dictionary> _messages = []; + private HashSet _mutedChats = []; public event Action? 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 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); diff --git a/Content.Client/_DV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs b/Content.Client/_DV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs index 37bba1d544..b6e7e4b647 100644 --- a/Content.Client/_DV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs +++ b/Content.Client/_DV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs @@ -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? 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; diff --git a/Content.Server/_DV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs b/Content.Server/_DV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs index 9ed253b28d..18ae56eecb 100644 --- a/Content.Server/_DV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs +++ b/Content.Server/_DV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs @@ -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 } } + /// + /// Handles editing the current chat conversation. + /// + private void HandleEditChat(Entity 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); + } + /// /// Handles closing the current chat conversation. /// @@ -229,6 +290,14 @@ public sealed class NanoChatCartridgeSystem : EntitySystem UpdateUIForCard(card); } + private void HandleToggleMuteChat(Entity card, NanoChatUiMessageEvent msg) + { + if (msg.RecipientNumber is not uint chat) + return; + _nanoChat.ToggleChatMuted((card, card.Comp), chat); + UpdateUIForCard(card); + } + private void HandleToggleListNumber(Entity 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 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 /// /// Handles unread message notifications and updates unread status. /// - private void HandleUnreadNotification(Entity recipient, NanoChatMessage message) + private void HandleUnreadNotification(Entity 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(); - 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 mutedChats = recipient.Comp.MutedChats; + if (recipient.Comp.NotificationsMuted || + mutedChats.Contains(message.SenderId) || + recipient.Comp.PdaUid is not { } pdaUid || + !TryComp(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(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); } /// @@ -505,16 +597,6 @@ public sealed class NanoChatCartridgeSystem : EntitySystem return null; } - /// - /// Truncates a message to the notification maximum length. - /// - private static string TruncateMessage(string message) - { - return message.Length <= NotificationMaxLength - ? message - : message[..(NotificationMaxLength - 4)] + " [...]"; - } - private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args) { _cartridge.RegisterBackgroundProgram(args.Loader, ent); @@ -547,6 +629,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem var recipients = new Dictionary(); var messages = new Dictionary>(); + var mutedChats = new HashSet(); 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, diff --git a/Content.Server/_DV/NanoChat/NanoChatSystem.cs b/Content.Server/_DV/NanoChat/NanoChatSystem.cs index 7d04d2f918..e8225a7bf0 100644 --- a/Content.Server/_DV/NanoChat/NanoChatSystem.cs +++ b/Content.Server/_DV/NanoChat/NanoChatSystem.cs @@ -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(OnInserted); + SubscribeLocalEvent(OnRemoved); + SubscribeLocalEvent(OnCardInit); SubscribeLocalEvent(OnMicrowaved, after: [typeof(IdCardSystem)]); } + private void OnInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != PdaComponent.PdaIdSlotId) + return; + + ent.Comp.PdaUid = args.Container.Owner; + Dirty(ent); + } + + private void OnRemoved(Entity ent, ref EntGotRemovedFromContainerMessage args) + { + if (args.Container.ID != PdaComponent.PdaIdSlotId) + return; + + ent.Comp.PdaUid = null; + Dirty(ent); + } + private void OnMicrowaved(Entity ent, ref BeingMicrowavedEvent args) { // Skip if the entity was deleted (e.g., by ID card system burning it) diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index c4ea0df7f8..018cab4dc8 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -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"; diff --git a/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs b/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs index daa843a933..b0fb8971c8 100644 --- a/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs +++ b/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs @@ -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; + /// /// When the message was sent. /// diff --git a/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiState.cs b/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiState.cs index a472315071..c24b75d4ef 100644 --- a/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiState.cs +++ b/Content.Shared/_DV/CartridgeLoader/Cartridges/NanoChatUiState.cs @@ -5,8 +5,9 @@ namespace Content.Shared._DV.CartridgeLoader.Cartridges; [Serializable, NetSerializable] public sealed class NanoChatUiState : BoundUserInterfaceState { - public readonly Dictionary Recipients = new(); - public readonly Dictionary> Messages = new(); + public readonly Dictionary Recipients = []; + public readonly Dictionary> Messages = []; + public readonly HashSet MutedChats = []; public readonly List? Contacts; public readonly uint? CurrentChat; public readonly uint OwnNumber; @@ -17,6 +18,7 @@ public sealed class NanoChatUiState : BoundUserInterfaceState public NanoChatUiState( Dictionary recipients, Dictionary> messages, + HashSet mutedChats, List? 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; diff --git a/Content.Shared/_DV/NanoChat/NanoChatCardComponent.cs b/Content.Shared/_DV/NanoChat/NanoChatCardComponent.cs index f926197346..81447ce53a 100644 --- a/Content.Shared/_DV/NanoChat/NanoChatCardComponent.cs +++ b/Content.Shared/_DV/NanoChat/NanoChatCardComponent.cs @@ -25,13 +25,19 @@ public sealed partial class NanoChatCardComponent : Component /// All chat recipients stored on this card. /// [DataField] - public Dictionary Recipients = new(); + public Dictionary Recipients = []; /// /// All messages stored on this card, keyed by recipient number. /// [DataField] - public Dictionary> Messages = new(); + public Dictionary> Messages = []; + + /// + /// The NanoChat numbers that should not give a notification, even when notifications are enabled. + /// + [DataField] + public HashSet MutedChats = []; /// /// The currently selected chat recipient number. @@ -62,4 +68,10 @@ public sealed partial class NanoChatCardComponent : Component /// [DataField] public bool ListNumber = true; + + /// + /// The PDA that this card is currently inserted to. + /// + [DataField] + public EntityUid? PdaUid = null; } diff --git a/Content.Shared/_DV/NanoChat/SharedNanoChatSystem.cs b/Content.Shared/_DV/NanoChat/SharedNanoChatSystem.cs index 00af522bac..cd88e8a07b 100644 --- a/Content.Shared/_DV/NanoChat/SharedNanoChatSystem.cs +++ b/Content.Shared/_DV/NanoChat/SharedNanoChatSystem.cs @@ -31,6 +31,16 @@ public abstract class SharedNanoChatSystem : EntitySystem args.PushMarkup(Loc.GetString("nanochat-card-examine-number", ("number", $"{ent.Comp.Number:D4}"))); } + /// + /// Helper Method for truncating a string to maximum length + /// + public static string Truncate(string text, int maxLength, string overflowText = "...") + { + return text.Length > maxLength + ? text[..(maxLength - overflowText.Length)] + overflowText + : text; + } + #region Public API Methods /// @@ -188,6 +198,19 @@ public abstract class SharedNanoChatSystem : EntitySystem Dirty(card); } + /// + /// Sets whether notifications are muted for a specific chat. + /// + public void ToggleChatMuted(Entity card, uint chat) + { + if (!Resolve(card, ref card.Comp)) + return; + + if (!card.Comp.MutedChats.Remove(chat)) + card.Comp.MutedChats.Add(chat); + Dirty(card); + } + /// /// Gets whether NanoChat number is listed. /// diff --git a/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl index 0515dbbb04..1825748e23 100644 --- a/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/_DV/cartridge-loader/cartridges.ftl @@ -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 diff --git a/Resources/Locale/en-US/_DV/escape-menu/options-menu.ftl b/Resources/Locale/en-US/_DV/escape-menu/options-menu.ftl index 725272c8a7..d92cffcc7b 100644 --- a/Resources/Locale/en-US/_DV/escape-menu/options-menu.ftl +++ b/Resources/Locale/en-US/_DV/escape-menu/options-menu.ftl @@ -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 diff --git a/Resources/Textures/_DV/Interface/VerbIcons/ATTRIBUTION.txt b/Resources/Textures/_DV/Interface/VerbIcons/ATTRIBUTION.txt index bdb60cbe64..992a7f1226 100644 --- a/Resources/Textures/_DV/Interface/VerbIcons/ATTRIBUTION.txt +++ b/Resources/Textures/_DV/Interface/VerbIcons/ATTRIBUTION.txt @@ -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 diff --git a/Resources/Textures/_DV/Interface/VerbIcons/edit.svg.png b/Resources/Textures/_DV/Interface/VerbIcons/edit.svg.png new file mode 100644 index 0000000000..270e1e7a1a Binary files /dev/null and b/Resources/Textures/_DV/Interface/VerbIcons/edit.svg.png differ diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index c6bb9d6155..a7d9a57946 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -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