From 224b4cf354a4a06308e5400bbe96ed2c0a1c6388 Mon Sep 17 00:00:00 2001 From: Ermucat Date: Wed, 25 Dec 2024 11:31:37 -0500 Subject: [PATCH 01/34] ALright basic system online, all systems operational hopefully --- Content.Client/Content.Client.csproj | 3 + .../Cartridges/NanoChatEntry.xaml | 48 ++ .../Cartridges/NanoChatEntry.xaml.cs | 39 ++ .../Cartridges/NanoChatLogEntry.xaml | 21 + .../Cartridges/NanoChatLogEntry.xaml.cs | 17 + .../Cartridges/NanoChatMessageBubble.xaml | 55 ++ .../Cartridges/NanoChatMessageBubble.xaml.cs | 62 +++ .../CartridgeLoader/Cartridges/NanoChatUi.cs | 43 ++ .../Cartridges/NanoChatUiFragment.xaml | 167 ++++++ .../Cartridges/NanoChatUiFragment.xaml.cs | 254 +++++++++ .../Cartridges/NewChatPopup.xaml | 52 ++ .../Cartridges/NewChatPopup.xaml.cs | 87 +++ .../Cartridges/NanoChatCartridgeComponent.cs | 26 + .../Cartridges/NanoChatCartridgeSystem.cs | 514 ++++++++++++++++++ .../Cartridges/NanoChatUiMessageEvent.cs | 166 ++++++ .../Cartridges/NanoChatUiState.cs | 30 + .../_DeltaV/Nanochat/NanoChatCardComponent.cs | 52 ++ .../_DeltaV/Nanochat/SharedNanoChatSystem.cs | 273 ++++++++++ 18 files changed, 1909 insertions(+) create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml.cs create mode 100644 Content.Server/_DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeComponent.cs create mode 100644 Content.Server/_DeltaV/CartridgeLoader/Cartridges/NanoChatCartridgeSystem.cs create mode 100644 Content.Shared/_DeltaV/CartridgeLoader/Cartridges/NanoChatUiMessageEvent.cs create mode 100644 Content.Shared/_DeltaV/CartridgeLoader/Cartridges/NanoChatUiState.cs create mode 100644 Content.Shared/_DeltaV/Nanochat/NanoChatCardComponent.cs create mode 100644 Content.Shared/_DeltaV/Nanochat/SharedNanoChatSystem.cs diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 43fd8df2e19a..b0b52393352b 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -23,6 +23,9 @@ + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml new file mode 100644 index 000000000000..0b1361336248 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml @@ -0,0 +1,48 @@ + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs new file mode 100644 index 000000000000..ff4ea9ba9c1d --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs @@ -0,0 +1,39 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatEntry : BoxContainer +{ + public event Action? OnPressed; + private uint _number; + private Action? _pressHandler; + + public NanoChatEntry() + { + RobustXamlLoader.Load(this); + } + + public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected) + { + // Remove old handler if it exists + if (_pressHandler != null) + ChatButton.OnPressed -= _pressHandler; + + _number = number; + + // Create and store new handler + _pressHandler = _ => OnPressed?.Invoke(_number); + ChatButton.OnPressed += _pressHandler; + + NameLabel.Text = recipient.Name; + JobLabel.Text = recipient.JobTitle ?? ""; + JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle); + UnreadIndicator.Visible = recipient.HasUnread; + + ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml new file mode 100644 index 000000000000..c87478d6301a --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml @@ -0,0 +1,21 @@ + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs new file mode 100644 index 000000000000..b94ea1a18aa1 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs @@ -0,0 +1,17 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatLogEntry : BoxContainer +{ + public NanoChatLogEntry(int number, string time, string message) + { + RobustXamlLoader.Load(this); + NumberLabel.Text = number.ToString(); + TimeLabel.Text = time; + MessageLabel.Text = message; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml new file mode 100644 index 000000000000..84daa2f1c595 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs new file mode 100644 index 000000000000..42725bb09c5a --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs @@ -0,0 +1,62 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatMessageBubble : BoxContainer +{ + public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green + public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray + public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border + public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white + public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red + + public NanoChatMessageBubble() + { + RobustXamlLoader.Load(this); + } + + public void SetMessage(NanoChatMessage message, bool isOwnMessage) + { + if (MessagePanel.PanelOverride is not StyleBoxFlat) + return; + + // Configure message appearance + var style = (StyleBoxFlat)MessagePanel.PanelOverride; + style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor; + style.BorderColor = BorderColor; + + // Set message content + MessageText.Text = message.Content; + MessageText.Modulate = TextColor; + + // Show delivery failed text if needed (only for own messages) + DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed; + if (DeliveryFailedLabel.Visible) + DeliveryFailedLabel.Modulate = ErrorColor; + + // For own messages: FlexSpace -> MessagePanel -> RightSpacer + // For other messages: LeftSpacer -> MessagePanel -> FlexSpace + MessageContainer.RemoveAllChildren(); + + // fuuuuuck + MessageBox.Parent?.RemoveChild(MessageBox); + + if (isOwnMessage) + { + MessageContainer.AddChild(FlexSpace); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(RightSpacer); + } + else + { + MessageContainer.AddChild(LeftSpacer); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(FlexSpace); + } + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs new file mode 100644 index 000000000000..fb65b03e8879 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs @@ -0,0 +1,43 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class NanoChatUi : UIFragment +{ + private NanoChatUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new NanoChatUiFragment(); + + _fragment.OnMessageSent += (type, number, content, job) => + { + SendNanoChatUiMessage(type, number, content, job, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is NanoChatUiState cast) + _fragment?.UpdateState(cast); + } + + private static void SendNanoChatUiMessage(NanoChatUiMessageType type, + uint? number, + string? content, + string? job, + BoundUserInterface userInterface) + { + var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job); + var message = new CartridgeUiMessage(nanoChatMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml new file mode 100644 index 000000000000..2a39094b85d3 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs new file mode 100644 index 000000000000..159d6b1a93ac --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs @@ -0,0 +1,254 @@ +using System.Linq; +using System.Numerics; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Timing; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatUiFragment : BoxContainer +{ + [Dependency] private readonly IGameTiming _timing = default!; + + private const int MaxMessageLength = 256; + + private readonly NewChatPopup _newChatPopup; + private uint? _currentChat; + private uint? _pendingChat; + private uint _ownNumber; + private bool _notificationsMuted; + private Dictionary _recipients = new(); + private Dictionary> _messages = new(); + + public event Action? OnMessageSent; + + public NanoChatUiFragment() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _newChatPopup = new NewChatPopup(); + SetupEventHandlers(); + } + + private void SetupEventHandlers() + { + _newChatPopup.OnChatCreated += (number, name, job) => + { + OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); + }; + + NewChatButton.OnPressed += _ => + { + _newChatPopup.ClearInputs(); + _newChatPopup.OpenCentered(); + }; + + MuteButton.OnPressed += _ => + { + _notificationsMuted = !_notificationsMuted; + UpdateMuteButton(); + OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null); + }; + + MessageInput.OnTextChanged += args => + { + var length = args.Text.Length; + var isValid = !string.IsNullOrWhiteSpace(args.Text) && + length <= MaxMessageLength && + (_currentChat != null || _pendingChat != null); + + SendButton.Disabled = !isValid; + + // Show character count when over limit + CharacterCount.Visible = length > MaxMessageLength; + if (length > MaxMessageLength) + { + CharacterCount.Text = Loc.GetString("nano-chat-message-too-long", + ("current", length), + ("max", MaxMessageLength)); + CharacterCount.StyleClasses.Add("LabelDanger"); + } + }; + + SendButton.OnPressed += _ => SendMessage(); + DeleteChatButton.OnPressed += _ => DeleteCurrentChat(); + } + + private void SendMessage() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text)) + return; + + var messageContent = MessageInput.Text; + + // Add predicted message + var predictedMessage = new NanoChatMessage( + _timing.CurTime, + messageContent, + _ownNumber + ); + + if (!_messages.TryGetValue(activeChat.Value, out var value)) + { + value = new List(); + _messages[activeChat.Value] = value; + } + + value.Add(predictedMessage); + + // Update UI with predicted message + UpdateMessages(_messages); + + // Send message event + OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null); + + // Clear input + MessageInput.Text = string.Empty; + SendButton.Disabled = true; + } + + private void SelectChat(uint number) + { + // Don't reselect the same chat + if (_currentChat == number && _pendingChat == null) + return; + + _pendingChat = number; + + // Predict marking messages as read + if (_recipients.TryGetValue(number, out var recipient)) + { + recipient.HasUnread = false; + _recipients[number] = recipient; + UpdateChatList(_recipients); + } + + OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null); + UpdateCurrentChat(); + } + + private void DeleteCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null) + return; + + OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null); + } + + private void UpdateChatList(Dictionary recipients) + { + ChatList.RemoveAllChildren(); + _recipients = recipients; + + NoChatsLabel.Visible = recipients.Count == 0; + if (NoChatsLabel.Parent != ChatList) + { + NoChatsLabel.Parent?.RemoveChild(NoChatsLabel); + ChatList.AddChild(NoChatsLabel); + } + + foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name)) + { + var entry = new NanoChatEntry(); + // For pending chat selection, always show it as selected even if unconfirmed + var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number); + entry.SetRecipient(recipient, number, isSelected); + entry.OnPressed += SelectChat; + ChatList.AddChild(entry); + } + } + + private void UpdateCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + var hasActiveChat = activeChat != null; + + // Update UI state + MessagesScroll.Visible = hasActiveChat; + CurrentChatName.Visible = !hasActiveChat; + MessageInputContainer.Visible = hasActiveChat; + DeleteChatButton.Visible = hasActiveChat; + DeleteChatButton.Disabled = !hasActiveChat; + + if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient)) + { + CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})"); + } + else + { + CurrentChatName.Text = Loc.GetString("nano-chat-select-chat"); + } + } + + private void UpdateMessages(Dictionary> messages) + { + _messages = messages; + MessageList.RemoveAllChildren(); + + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages)) + return; + + foreach (var message in chatMessages) + { + var messageBubble = new NanoChatMessageBubble(); + messageBubble.SetMessage(message, message.SenderId == _ownNumber); + MessageList.AddChild(messageBubble); + + // Add spacing between messages + MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) }); + } + + MessageList.InvalidateMeasure(); + MessagesScroll.InvalidateMeasure(); + + // Scroll to bottom after messages are added + if (MessageList.Parent is ScrollContainer scroll) + scroll.SetScrollValue(new Vector2(0, float.MaxValue)); + } + + private void UpdateMuteButton() + { + if (BellMutedIcon != null) + BellMutedIcon.Visible = _notificationsMuted; + } + + public void UpdateState(NanoChatUiState state) + { + _ownNumber = state.OwnNumber; + _notificationsMuted = state.NotificationsMuted; + OwnNumberLabel.Text = $"#{state.OwnNumber:D4}"; + UpdateMuteButton(); + + // Update new chat button state based on recipient limit + var atLimit = state.Recipients.Count >= state.MaxRecipients; + NewChatButton.Disabled = atLimit; + NewChatButton.ToolTip = atLimit + ? Loc.GetString("nano-chat-max-recipients") + : Loc.GetString("nano-chat-new-chat"); + + // First handle pending chat resolution if we have one + if (_pendingChat != null) + { + if (_pendingChat == state.CurrentChat) + _currentChat = _pendingChat; // Server confirmed our selection + + _pendingChat = null; // Clear pending either way + } + + // No pending chat or it was just cleared, update current directly + if (_pendingChat == null) + _currentChat = state.CurrentChat; + + UpdateCurrentChat(); + UpdateChatList(state.Recipients); + UpdateMessages(state.Messages); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml new file mode 100644 index 000000000000..20095c4fce99 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs new file mode 100644 index 000000000000..ff4ea9ba9c1d --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs @@ -0,0 +1,39 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatEntry : BoxContainer +{ + public event Action? OnPressed; + private uint _number; + private Action? _pressHandler; + + public NanoChatEntry() + { + RobustXamlLoader.Load(this); + } + + public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected) + { + // Remove old handler if it exists + if (_pressHandler != null) + ChatButton.OnPressed -= _pressHandler; + + _number = number; + + // Create and store new handler + _pressHandler = _ => OnPressed?.Invoke(_number); + ChatButton.OnPressed += _pressHandler; + + NameLabel.Text = recipient.Name; + JobLabel.Text = recipient.JobTitle ?? ""; + JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle); + UnreadIndicator.Visible = recipient.HasUnread; + + ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml new file mode 100644 index 000000000000..c87478d6301a --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml @@ -0,0 +1,21 @@ + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs new file mode 100644 index 000000000000..b94ea1a18aa1 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs @@ -0,0 +1,17 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatLogEntry : BoxContainer +{ + public NanoChatLogEntry(int number, string time, string message) + { + RobustXamlLoader.Load(this); + NumberLabel.Text = number.ToString(); + TimeLabel.Text = time; + MessageLabel.Text = message; + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml new file mode 100644 index 000000000000..84daa2f1c595 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs new file mode 100644 index 000000000000..42725bb09c5a --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs @@ -0,0 +1,62 @@ +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatMessageBubble : BoxContainer +{ + public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green + public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray + public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border + public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white + public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red + + public NanoChatMessageBubble() + { + RobustXamlLoader.Load(this); + } + + public void SetMessage(NanoChatMessage message, bool isOwnMessage) + { + if (MessagePanel.PanelOverride is not StyleBoxFlat) + return; + + // Configure message appearance + var style = (StyleBoxFlat)MessagePanel.PanelOverride; + style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor; + style.BorderColor = BorderColor; + + // Set message content + MessageText.Text = message.Content; + MessageText.Modulate = TextColor; + + // Show delivery failed text if needed (only for own messages) + DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed; + if (DeliveryFailedLabel.Visible) + DeliveryFailedLabel.Modulate = ErrorColor; + + // For own messages: FlexSpace -> MessagePanel -> RightSpacer + // For other messages: LeftSpacer -> MessagePanel -> FlexSpace + MessageContainer.RemoveAllChildren(); + + // fuuuuuck + MessageBox.Parent?.RemoveChild(MessageBox); + + if (isOwnMessage) + { + MessageContainer.AddChild(FlexSpace); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(RightSpacer); + } + else + { + MessageContainer.AddChild(LeftSpacer); + MessageContainer.AddChild(MessageBox); + MessageContainer.AddChild(FlexSpace); + } + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs new file mode 100644 index 000000000000..fb65b03e8879 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs @@ -0,0 +1,43 @@ +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.UserInterface; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class NanoChatUi : UIFragment +{ + private NanoChatUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new NanoChatUiFragment(); + + _fragment.OnMessageSent += (type, number, content, job) => + { + SendNanoChatUiMessage(type, number, content, job, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is NanoChatUiState cast) + _fragment?.UpdateState(cast); + } + + private static void SendNanoChatUiMessage(NanoChatUiMessageType type, + uint? number, + string? content, + string? job, + BoundUserInterface userInterface) + { + var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job); + var message = new CartridgeUiMessage(nanoChatMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml new file mode 100644 index 000000000000..2a39094b85d3 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs new file mode 100644 index 000000000000..159d6b1a93ac --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs @@ -0,0 +1,254 @@ +using System.Linq; +using System.Numerics; +using Content.Shared.DeltaV.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Timing; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class NanoChatUiFragment : BoxContainer +{ + [Dependency] private readonly IGameTiming _timing = default!; + + private const int MaxMessageLength = 256; + + private readonly NewChatPopup _newChatPopup; + private uint? _currentChat; + private uint? _pendingChat; + private uint _ownNumber; + private bool _notificationsMuted; + private Dictionary _recipients = new(); + private Dictionary> _messages = new(); + + public event Action? OnMessageSent; + + public NanoChatUiFragment() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _newChatPopup = new NewChatPopup(); + SetupEventHandlers(); + } + + private void SetupEventHandlers() + { + _newChatPopup.OnChatCreated += (number, name, job) => + { + OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); + }; + + NewChatButton.OnPressed += _ => + { + _newChatPopup.ClearInputs(); + _newChatPopup.OpenCentered(); + }; + + MuteButton.OnPressed += _ => + { + _notificationsMuted = !_notificationsMuted; + UpdateMuteButton(); + OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null); + }; + + MessageInput.OnTextChanged += args => + { + var length = args.Text.Length; + var isValid = !string.IsNullOrWhiteSpace(args.Text) && + length <= MaxMessageLength && + (_currentChat != null || _pendingChat != null); + + SendButton.Disabled = !isValid; + + // Show character count when over limit + CharacterCount.Visible = length > MaxMessageLength; + if (length > MaxMessageLength) + { + CharacterCount.Text = Loc.GetString("nano-chat-message-too-long", + ("current", length), + ("max", MaxMessageLength)); + CharacterCount.StyleClasses.Add("LabelDanger"); + } + }; + + SendButton.OnPressed += _ => SendMessage(); + DeleteChatButton.OnPressed += _ => DeleteCurrentChat(); + } + + private void SendMessage() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text)) + return; + + var messageContent = MessageInput.Text; + + // Add predicted message + var predictedMessage = new NanoChatMessage( + _timing.CurTime, + messageContent, + _ownNumber + ); + + if (!_messages.TryGetValue(activeChat.Value, out var value)) + { + value = new List(); + _messages[activeChat.Value] = value; + } + + value.Add(predictedMessage); + + // Update UI with predicted message + UpdateMessages(_messages); + + // Send message event + OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null); + + // Clear input + MessageInput.Text = string.Empty; + SendButton.Disabled = true; + } + + private void SelectChat(uint number) + { + // Don't reselect the same chat + if (_currentChat == number && _pendingChat == null) + return; + + _pendingChat = number; + + // Predict marking messages as read + if (_recipients.TryGetValue(number, out var recipient)) + { + recipient.HasUnread = false; + _recipients[number] = recipient; + UpdateChatList(_recipients); + } + + OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null); + UpdateCurrentChat(); + } + + private void DeleteCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null) + return; + + OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null); + } + + private void UpdateChatList(Dictionary recipients) + { + ChatList.RemoveAllChildren(); + _recipients = recipients; + + NoChatsLabel.Visible = recipients.Count == 0; + if (NoChatsLabel.Parent != ChatList) + { + NoChatsLabel.Parent?.RemoveChild(NoChatsLabel); + ChatList.AddChild(NoChatsLabel); + } + + foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name)) + { + var entry = new NanoChatEntry(); + // For pending chat selection, always show it as selected even if unconfirmed + var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number); + entry.SetRecipient(recipient, number, isSelected); + entry.OnPressed += SelectChat; + ChatList.AddChild(entry); + } + } + + private void UpdateCurrentChat() + { + var activeChat = _pendingChat ?? _currentChat; + var hasActiveChat = activeChat != null; + + // Update UI state + MessagesScroll.Visible = hasActiveChat; + CurrentChatName.Visible = !hasActiveChat; + MessageInputContainer.Visible = hasActiveChat; + DeleteChatButton.Visible = hasActiveChat; + DeleteChatButton.Disabled = !hasActiveChat; + + if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient)) + { + CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})"); + } + else + { + CurrentChatName.Text = Loc.GetString("nano-chat-select-chat"); + } + } + + private void UpdateMessages(Dictionary> messages) + { + _messages = messages; + MessageList.RemoveAllChildren(); + + var activeChat = _pendingChat ?? _currentChat; + if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages)) + return; + + foreach (var message in chatMessages) + { + var messageBubble = new NanoChatMessageBubble(); + messageBubble.SetMessage(message, message.SenderId == _ownNumber); + MessageList.AddChild(messageBubble); + + // Add spacing between messages + MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) }); + } + + MessageList.InvalidateMeasure(); + MessagesScroll.InvalidateMeasure(); + + // Scroll to bottom after messages are added + if (MessageList.Parent is ScrollContainer scroll) + scroll.SetScrollValue(new Vector2(0, float.MaxValue)); + } + + private void UpdateMuteButton() + { + if (BellMutedIcon != null) + BellMutedIcon.Visible = _notificationsMuted; + } + + public void UpdateState(NanoChatUiState state) + { + _ownNumber = state.OwnNumber; + _notificationsMuted = state.NotificationsMuted; + OwnNumberLabel.Text = $"#{state.OwnNumber:D4}"; + UpdateMuteButton(); + + // Update new chat button state based on recipient limit + var atLimit = state.Recipients.Count >= state.MaxRecipients; + NewChatButton.Disabled = atLimit; + NewChatButton.ToolTip = atLimit + ? Loc.GetString("nano-chat-max-recipients") + : Loc.GetString("nano-chat-new-chat"); + + // First handle pending chat resolution if we have one + if (_pendingChat != null) + { + if (_pendingChat == state.CurrentChat) + _currentChat = _pendingChat; // Server confirmed our selection + + _pendingChat = null; // Clear pending either way + } + + // No pending chat or it was just cleared, update current directly + if (_pendingChat == null) + _currentChat = state.CurrentChat; + + UpdateCurrentChat(); + UpdateChatList(state.Recipients); + UpdateMessages(state.Messages); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml new file mode 100644 index 000000000000..20095c4fce99 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + - diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs deleted file mode 100644 index ff4ea9ba9c1d..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatEntry.xaml.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Content.Shared.DeltaV.CartridgeLoader.Cartridges; -using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; - -namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; - -[GenerateTypedNameReferences] -public sealed partial class NanoChatEntry : BoxContainer -{ - public event Action? OnPressed; - private uint _number; - private Action? _pressHandler; - - public NanoChatEntry() - { - RobustXamlLoader.Load(this); - } - - public void SetRecipient(NanoChatRecipient recipient, uint number, bool isSelected) - { - // Remove old handler if it exists - if (_pressHandler != null) - ChatButton.OnPressed -= _pressHandler; - - _number = number; - - // Create and store new handler - _pressHandler = _ => OnPressed?.Invoke(_number); - ChatButton.OnPressed += _pressHandler; - - NameLabel.Text = recipient.Name; - JobLabel.Text = recipient.JobTitle ?? ""; - JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle); - UnreadIndicator.Visible = recipient.HasUnread; - - ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null; - } -} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml deleted file mode 100644 index c87478d6301a..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs deleted file mode 100644 index b94ea1a18aa1..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatLogEntry.xaml.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; - -namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; - -[GenerateTypedNameReferences] -public sealed partial class NanoChatLogEntry : BoxContainer -{ - public NanoChatLogEntry(int number, string time, string message) - { - RobustXamlLoader.Load(this); - NumberLabel.Text = number.ToString(); - TimeLabel.Text = time; - MessageLabel.Text = message; - } -} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml deleted file mode 100644 index 84daa2f1c595..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs deleted file mode 100644 index 42725bb09c5a..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatMessageBubble.xaml.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Content.Shared.DeltaV.CartridgeLoader.Cartridges; -using Robust.Client.AutoGenerated; -using Robust.Client.Graphics; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; - -namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; - -[GenerateTypedNameReferences] -public sealed partial class NanoChatMessageBubble : BoxContainer -{ - public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green - public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray - public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border - public static readonly Color TextColor = Color.FromHex("#dcdcdc"); // Slightly softened white - public static readonly Color ErrorColor = Color.FromHex("#cc3333"); // Red - - public NanoChatMessageBubble() - { - RobustXamlLoader.Load(this); - } - - public void SetMessage(NanoChatMessage message, bool isOwnMessage) - { - if (MessagePanel.PanelOverride is not StyleBoxFlat) - return; - - // Configure message appearance - var style = (StyleBoxFlat)MessagePanel.PanelOverride; - style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor; - style.BorderColor = BorderColor; - - // Set message content - MessageText.Text = message.Content; - MessageText.Modulate = TextColor; - - // Show delivery failed text if needed (only for own messages) - DeliveryFailedLabel.Visible = isOwnMessage && message.DeliveryFailed; - if (DeliveryFailedLabel.Visible) - DeliveryFailedLabel.Modulate = ErrorColor; - - // For own messages: FlexSpace -> MessagePanel -> RightSpacer - // For other messages: LeftSpacer -> MessagePanel -> FlexSpace - MessageContainer.RemoveAllChildren(); - - // fuuuuuck - MessageBox.Parent?.RemoveChild(MessageBox); - - if (isOwnMessage) - { - MessageContainer.AddChild(FlexSpace); - MessageContainer.AddChild(MessageBox); - MessageContainer.AddChild(RightSpacer); - } - else - { - MessageContainer.AddChild(LeftSpacer); - MessageContainer.AddChild(MessageBox); - MessageContainer.AddChild(FlexSpace); - } - } -} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs deleted file mode 100644 index fb65b03e8879..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUi.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Client.UserInterface.Fragments; -using Content.Shared.CartridgeLoader; -using Content.Shared.DeltaV.CartridgeLoader.Cartridges; -using Robust.Client.UserInterface; - -namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; - -public sealed partial class NanoChatUi : UIFragment -{ - private NanoChatUiFragment? _fragment; - - public override Control GetUIFragmentRoot() - { - return _fragment!; - } - - public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) - { - _fragment = new NanoChatUiFragment(); - - _fragment.OnMessageSent += (type, number, content, job) => - { - SendNanoChatUiMessage(type, number, content, job, userInterface); - }; - } - - public override void UpdateState(BoundUserInterfaceState state) - { - if (state is NanoChatUiState cast) - _fragment?.UpdateState(cast); - } - - private static void SendNanoChatUiMessage(NanoChatUiMessageType type, - uint? number, - string? content, - string? job, - BoundUserInterface userInterface) - { - var nanoChatMessage = new NanoChatUiMessageEvent(type, number, content, job); - var message = new CartridgeUiMessage(nanoChatMessage); - userInterface.SendMessage(message); - } -} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml deleted file mode 100644 index 2a39094b85d3..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs deleted file mode 100644 index 159d6b1a93ac..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NanoChatUiFragment.xaml.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Shared.DeltaV.CartridgeLoader.Cartridges; -using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Client.UserInterface; -using Robust.Shared.Timing; - -namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; - -[GenerateTypedNameReferences] -public sealed partial class NanoChatUiFragment : BoxContainer -{ - [Dependency] private readonly IGameTiming _timing = default!; - - private const int MaxMessageLength = 256; - - private readonly NewChatPopup _newChatPopup; - private uint? _currentChat; - private uint? _pendingChat; - private uint _ownNumber; - private bool _notificationsMuted; - private Dictionary _recipients = new(); - private Dictionary> _messages = new(); - - public event Action? OnMessageSent; - - public NanoChatUiFragment() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - - _newChatPopup = new NewChatPopup(); - SetupEventHandlers(); - } - - private void SetupEventHandlers() - { - _newChatPopup.OnChatCreated += (number, name, job) => - { - OnMessageSent?.Invoke(NanoChatUiMessageType.NewChat, number, name, job); - }; - - NewChatButton.OnPressed += _ => - { - _newChatPopup.ClearInputs(); - _newChatPopup.OpenCentered(); - }; - - MuteButton.OnPressed += _ => - { - _notificationsMuted = !_notificationsMuted; - UpdateMuteButton(); - OnMessageSent?.Invoke(NanoChatUiMessageType.ToggleMute, null, null, null); - }; - - MessageInput.OnTextChanged += args => - { - var length = args.Text.Length; - var isValid = !string.IsNullOrWhiteSpace(args.Text) && - length <= MaxMessageLength && - (_currentChat != null || _pendingChat != null); - - SendButton.Disabled = !isValid; - - // Show character count when over limit - CharacterCount.Visible = length > MaxMessageLength; - if (length > MaxMessageLength) - { - CharacterCount.Text = Loc.GetString("nano-chat-message-too-long", - ("current", length), - ("max", MaxMessageLength)); - CharacterCount.StyleClasses.Add("LabelDanger"); - } - }; - - SendButton.OnPressed += _ => SendMessage(); - DeleteChatButton.OnPressed += _ => DeleteCurrentChat(); - } - - private void SendMessage() - { - var activeChat = _pendingChat ?? _currentChat; - if (activeChat == null || string.IsNullOrWhiteSpace(MessageInput.Text)) - return; - - var messageContent = MessageInput.Text; - - // Add predicted message - var predictedMessage = new NanoChatMessage( - _timing.CurTime, - messageContent, - _ownNumber - ); - - if (!_messages.TryGetValue(activeChat.Value, out var value)) - { - value = new List(); - _messages[activeChat.Value] = value; - } - - value.Add(predictedMessage); - - // Update UI with predicted message - UpdateMessages(_messages); - - // Send message event - OnMessageSent?.Invoke(NanoChatUiMessageType.SendMessage, activeChat, messageContent, null); - - // Clear input - MessageInput.Text = string.Empty; - SendButton.Disabled = true; - } - - private void SelectChat(uint number) - { - // Don't reselect the same chat - if (_currentChat == number && _pendingChat == null) - return; - - _pendingChat = number; - - // Predict marking messages as read - if (_recipients.TryGetValue(number, out var recipient)) - { - recipient.HasUnread = false; - _recipients[number] = recipient; - UpdateChatList(_recipients); - } - - OnMessageSent?.Invoke(NanoChatUiMessageType.SelectChat, number, null, null); - UpdateCurrentChat(); - } - - private void DeleteCurrentChat() - { - var activeChat = _pendingChat ?? _currentChat; - if (activeChat == null) - return; - - OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null); - } - - private void UpdateChatList(Dictionary recipients) - { - ChatList.RemoveAllChildren(); - _recipients = recipients; - - NoChatsLabel.Visible = recipients.Count == 0; - if (NoChatsLabel.Parent != ChatList) - { - NoChatsLabel.Parent?.RemoveChild(NoChatsLabel); - ChatList.AddChild(NoChatsLabel); - } - - foreach (var (number, recipient) in recipients.OrderBy(r => r.Value.Name)) - { - var entry = new NanoChatEntry(); - // For pending chat selection, always show it as selected even if unconfirmed - var isSelected = (_pendingChat == number) || (_pendingChat == null && _currentChat == number); - entry.SetRecipient(recipient, number, isSelected); - entry.OnPressed += SelectChat; - ChatList.AddChild(entry); - } - } - - private void UpdateCurrentChat() - { - var activeChat = _pendingChat ?? _currentChat; - var hasActiveChat = activeChat != null; - - // Update UI state - MessagesScroll.Visible = hasActiveChat; - CurrentChatName.Visible = !hasActiveChat; - MessageInputContainer.Visible = hasActiveChat; - DeleteChatButton.Visible = hasActiveChat; - DeleteChatButton.Disabled = !hasActiveChat; - - if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient)) - { - CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})"); - } - else - { - CurrentChatName.Text = Loc.GetString("nano-chat-select-chat"); - } - } - - private void UpdateMessages(Dictionary> messages) - { - _messages = messages; - MessageList.RemoveAllChildren(); - - var activeChat = _pendingChat ?? _currentChat; - if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages)) - return; - - foreach (var message in chatMessages) - { - var messageBubble = new NanoChatMessageBubble(); - messageBubble.SetMessage(message, message.SenderId == _ownNumber); - MessageList.AddChild(messageBubble); - - // Add spacing between messages - MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) }); - } - - MessageList.InvalidateMeasure(); - MessagesScroll.InvalidateMeasure(); - - // Scroll to bottom after messages are added - if (MessageList.Parent is ScrollContainer scroll) - scroll.SetScrollValue(new Vector2(0, float.MaxValue)); - } - - private void UpdateMuteButton() - { - if (BellMutedIcon != null) - BellMutedIcon.Visible = _notificationsMuted; - } - - public void UpdateState(NanoChatUiState state) - { - _ownNumber = state.OwnNumber; - _notificationsMuted = state.NotificationsMuted; - OwnNumberLabel.Text = $"#{state.OwnNumber:D4}"; - UpdateMuteButton(); - - // Update new chat button state based on recipient limit - var atLimit = state.Recipients.Count >= state.MaxRecipients; - NewChatButton.Disabled = atLimit; - NewChatButton.ToolTip = atLimit - ? Loc.GetString("nano-chat-max-recipients") - : Loc.GetString("nano-chat-new-chat"); - - // First handle pending chat resolution if we have one - if (_pendingChat != null) - { - if (_pendingChat == state.CurrentChat) - _currentChat = _pendingChat; // Server confirmed our selection - - _pendingChat = null; // Clear pending either way - } - - // No pending chat or it was just cleared, update current directly - if (_pendingChat == null) - _currentChat = state.CurrentChat; - - UpdateCurrentChat(); - UpdateChatList(state.Recipients); - UpdateMessages(state.Messages); - } -} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml deleted file mode 100644 index 20095c4fce99..000000000000 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/NewChatPopup.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - -