Группки WhatsApp edition (#179)

* nanoext

* forensicup
This commit is contained in:
Zekins
2025-09-18 20:57:20 +03:00
committed by GitHub
parent 7b904b0ac2
commit 40dd5c3cd1
16 changed files with 669 additions and 38 deletions

View File

@@ -0,0 +1,15 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'nano-chat-ui-create-group-title'}"
MinWidth="500">
<BoxContainer Orientation="Vertical" Margin="4">
<Label Text="{Loc 'nano-chat-ui-group-name'}" StyleClasses="LabelHeading"/>
<LineEdit Name="GroupNameInput" PlaceHolder="{Loc 'nano-chat-ui-group-name-placeholder'}"/>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0">
<Button Name="CancelButton" Text="{Loc 'nano-chat-ui-cancel'}"/>
<Button Name="CreateButton" Text="{Loc 'nano-chat-ui-create'}"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,30 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Wega.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatCreateGroupPopup : DefaultWindow
{
public Action<string>? OnGroupCreated;
public NanoChatCreateGroupPopup()
{
RobustXamlLoader.Load(this);
CancelButton.OnPressed += _ => Close();
CreateButton.OnPressed += _ =>
{
if (!string.IsNullOrWhiteSpace(GroupNameInput.Text))
{
var groupName = GroupNameInput.Text.Length <= 16
? GroupNameInput.Text
: GroupNameInput.Text[..16];
OnGroupCreated?.Invoke(groupName);
Close();
}
};
}
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface;
namespace Content.Client._Wega.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatGroupControl : Control
{
public Action? OnPressed;
private readonly ChatGroup _group;
public NanoChatGroupControl(ChatGroup group)
{
RobustXamlLoader.Load(this);
_group = group;
var groupName = group.GroupName;
if (group.HasUnread)
groupName += Loc.GetString("nano-chat-ui-unread-indicator");
GroupName.Text = groupName;
MemberCount.Text = group.MemberCount.ToString();
MainButton.OnPressed += _ => OnPressed?.Invoke();
}
}

View File

@@ -0,0 +1,8 @@
<Control xmlns="https://spacestation14.io">
<Button Name="MainButton" StyleClasses="ButtonSquare">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="GroupName" HorizontalExpand="True"/>
<Label Name="MemberCount" StyleClasses="LabelSubText" Margin="4 0 0 0"/>
</BoxContainer>
</Button>
</Control>

View File

@@ -0,0 +1,15 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'nano-chat-ui-join-group-title'}"
MinWidth="500">
<BoxContainer Orientation="Vertical" Margin="4">
<Label Text="{Loc 'nano-chat-ui-group-id'}" StyleClasses="LabelHeading"/>
<LineEdit Name="GroupIdInput" PlaceHolder="{Loc 'nano-chat-ui-group-id-placeholder'}"/>
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0">
<Button Name="CancelButton" Text="{Loc 'nano-chat-ui-cancel'}"/>
<Button Name="JoinButton" Text="{Loc 'nano-chat-ui-join'}"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,41 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Wega.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatJoinGroupPopup : DefaultWindow
{
public Action<string>? OnGroupJoined;
public NanoChatJoinGroupPopup()
{
RobustXamlLoader.Load(this);
CancelButton.OnPressed += _ => Close();
JoinButton.OnPressed += _ =>
{
if (!string.IsNullOrWhiteSpace(GroupIdInput.Text))
{
var normalizedId = NormalizeGroupId(GroupIdInput.Text.Trim());
var groupId = normalizedId.Length <= 5
? normalizedId
: normalizedId[..5];
OnGroupJoined?.Invoke(groupId);
Close();
}
};
}
private string NormalizeGroupId(string contactId)
{
var cleanedId = contactId.Trim().Replace(" ", "");
if (!cleanedId.StartsWith("G") && cleanedId.All(char.IsDigit))
return "G" + cleanedId;
return cleanedId;
}
}

View File

@@ -9,6 +9,8 @@ public sealed partial class NanoChatUi : UIFragment
{
private NanoChatUiFragment? _fragment;
private NanoChatAddContactPopup? _addContactPopup;
private NanoChatJoinGroupPopup? _joinGroupPopup;
private NanoChatCreateGroupPopup? _createGroupPopup;
public override Control GetUIFragmentRoot() => _fragment!;
@@ -16,25 +18,36 @@ public sealed partial class NanoChatUi : UIFragment
{
_fragment = new NanoChatUiFragment();
_addContactPopup = new NanoChatAddContactPopup();
_joinGroupPopup = new NanoChatJoinGroupPopup();
_createGroupPopup = new NanoChatCreateGroupPopup();
_fragment.InitializeEmojiPicker();
_fragment.OpenAddContact += () => _addContactPopup.OpenCentered();
_fragment.JoinGroup += () => _joinGroupPopup.OpenCentered();
_fragment.CreateGroup += () => _createGroupPopup.OpenCentered();
_fragment.EraseChat += contactId =>
{
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatEraseContact(contactId))));
};
_fragment.LeaveChat += groupId =>
{
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatLeaveGroup(groupId))));
};
_fragment.OpenEmojiPicker += () => _fragment.OpenEmojiPickerInternal();
_fragment.OnMutePressed += () =>
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatMuted())));
_fragment.SetActiveChat += contactId =>
_fragment.SetActiveChat += chatId =>
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatSetActiveChat(contactId))));
new NanoChatUiMessageEvent(new NanoChatSetActiveChat(chatId))));
_fragment.SendMessage += message =>
{
@@ -51,6 +64,18 @@ public sealed partial class NanoChatUi : UIFragment
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatAddContact(contactId, contactName))));
};
_joinGroupPopup.OnGroupJoined += groupId =>
{
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatJoinGroup(groupId))));
};
_createGroupPopup.OnGroupCreated += groupName =>
{
userInterface.SendMessage(new CartridgeUiMessage(
new NanoChatUiMessageEvent(new NanoChatCreateGroup(groupName))));
};
}
public override void UpdateState(BoundUserInterfaceState state)

View File

@@ -22,6 +22,10 @@
<TextureButton Name="MuteChatButton" MaxSize="24 24" Margin="0 0 4 0"/>
<TextureButton Name="AddContactButton" MaxSize="24 24" ToolTip="{Loc 'nano-chat-ui-add-contact-tooltip'}"
TexturePath="/Textures/Interface/VerbIcons/plus.svg.192dpi.png" VerticalAlignment="Center"/>
<TextureButton Name="JoinGroupButton" MaxSize="24 24" ToolTip="{Loc 'nano-chat-ui-join-group-tooltip'}"
TexturePath="/Textures/Interface/VerbIcons/in.svg.192dpi.png" VerticalAlignment="Center" Margin="4 0 0 0"/>
<TextureButton Name="CreateGroupButton" MaxSize="24 24" ToolTip="{Loc 'nano-chat-ui-create-group-tooltip'}"
TexturePath="/Textures/Interface/VerbIcons/group.svg.192dpi.png" VerticalAlignment="Center" Margin="4 0 0 0"/>
</BoxContainer>
</controls:StripeBack>
</BoxContainer>
@@ -29,15 +33,23 @@
<!-- Main content -->
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<!-- Contacts list -->
<!-- Contacts/Groups panel -->
<PanelContainer StyleClasses="AngleRect" MinWidth="150" MaxWidth="200" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<!-- Tabs -->
<TabContainer Name="ChatTypeTabs" VerticalExpand="True">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Name="ContactsContainer" Orientation="Vertical" HorizontalExpand="True"/>
</ScrollContainer>
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Name="GroupsContainer" Orientation="Vertical" HorizontalExpand="True"/>
</ScrollContainer>
</TabContainer>
<Label Name="NoContactsLabel" Text="{Loc 'nano-chat-ui-no-contacts'}" StyleClasses="LabelSubText"
HorizontalAlignment="Center" VerticalAlignment="Center" Visible="False"/>
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Name="ContactsContainer" Orientation="Vertical" HorizontalExpand="True"/>
</ScrollContainer>
<Label Name="NoGroupsLabel" Text="{Loc 'nano-chat-ui-no-groups'}" StyleClasses="LabelSubText"
HorizontalAlignment="Center" VerticalAlignment="Center" Visible="False"/>
</BoxContainer>
</PanelContainer>
@@ -54,6 +66,8 @@
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 4">
<Label Name="ChatTitle" StyleClasses="LabelHeading" Margin="4 0 0 0" VerticalAlignment="Center" HorizontalExpand="True"/>
<TextureButton Name="LeaveChatButton" MaxSize="24 24" Margin="0 0 4 0" Visible="False" ToolTip="{Loc 'nano-chat-ui-leave-chat-tooltip'}"
TexturePath="/Textures/Interface/VerbIcons/close.svg.192dpi.png"/>
<TextureButton Name="EraseChatButton" MaxSize="24 24" Margin="0 0 4 0" Visible="False" ToolTip="{Loc 'nano-chat-ui-erase-chat-tooltip'}"
TexturePath="/Textures/Interface/eraser.svg.png"/>
<TextureButton Name="CloseChatButton" MaxSize="24 24" Visible="False" ToolTip="{Loc 'nano-chat-ui-close-chat-tooltip'}"

View File

@@ -19,18 +19,27 @@ public sealed partial class NanoChatUiFragment : BoxContainer
public Action<string>? SendMessage;
public Action<string>? EraseChat;
public Action<string>? CloseChat;
public Action<string>? LeaveChat;
public Action? JoinGroup;
public Action? CreateGroup;
private NanoChatEmojiPopup? _emojiPopup;
public string? ActiveChatId;
public Dictionary<string, ChatContact> Contacts = new();
public Dictionary<string, ChatGroup> Groups = new();
public NanoChatUiFragment()
{
RobustXamlLoader.Load(this);
Orientation = LayoutOrientation.Vertical;
ChatTypeTabs.SetTabTitle(0, Loc.GetString("nano-chat-ui-tab-contacts"));
ChatTypeTabs.SetTabTitle(1, Loc.GetString("nano-chat-ui-tab-groups"));
AddContactButton.OnPressed += _ => OpenAddContact?.Invoke();
JoinGroupButton.OnPressed += _ => JoinGroup?.Invoke();
CreateGroupButton.OnPressed += _ => CreateGroup?.Invoke();
MuteChatButton.OnPressed += _ => OnMutePressed?.Invoke();
EmojiButton.OnPressed += _ => OpenEmojiPicker?.Invoke();
@@ -52,6 +61,16 @@ public sealed partial class NanoChatUiFragment : BoxContainer
}
};
LeaveChatButton.OnPressed += _ =>
{
if (ActiveChatId != null && ActiveChatId.StartsWith("G"))
{
LeaveChat?.Invoke(ActiveChatId);
ActiveChatId = null;
UpdateUiState();
}
};
CloseChatButton.OnPressed += _ =>
{
if (ActiveChatId != null)
@@ -71,16 +90,24 @@ public sealed partial class NanoChatUiFragment : BoxContainer
}
};
ChatTypeTabs.OnTabChanged += _ => OnTabChanged();
EmojiButton.AddStyleClass(StyleNano.ButtonOpenBoth);
SendButton.AddStyleClass(StyleNano.ButtonOpenLeft);
UpdateUiState();
}
private void OnTabChanged()
{
UpdateContactsList();
}
public void UpdateState(NanoChatUiState state)
{
OwnIdLabel.Text = state.ChatId;
Contacts = state.Contacts;
Groups = state.Groups;
ActiveChatId = state.ActiveChat;
MuteChatButton.TexturePath = state.Muted
@@ -90,7 +117,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
? Loc.GetString("nano-chat-ui-unmute-tooltip")
: Loc.GetString("nano-chat-ui-mute-tooltip");
// Update contacts list
// Update contacts and groups lists
UpdateContactsList();
// Update messages if we have active chat
@@ -130,13 +157,28 @@ public sealed partial class NanoChatUiFragment : BoxContainer
private void UpdateContactsList()
{
ContactsContainer.RemoveAllChildren();
NoContactsLabel.Visible = Contacts.Count == 0;
GroupsContainer.RemoveAllChildren();
foreach (var contact in Contacts.Values.OrderBy(c => c.ContactName))
NoContactsLabel.Visible = Contacts.Count == 0 && ChatTypeTabs.CurrentTab == 0;
NoGroupsLabel.Visible = Groups.Count == 0 && ChatTypeTabs.CurrentTab == 1;
if (ChatTypeTabs.CurrentTab == 0)
{
var control = new NanoChatContactControl(contact);
control.OnPressed += () => SetActiveChat?.Invoke(contact.ContactId);
ContactsContainer.AddChild(control);
foreach (var contact in Contacts.Values.OrderBy(c => c.ContactName))
{
var control = new NanoChatContactControl(contact);
control.OnPressed += () => SetActiveChat?.Invoke(contact.ContactId);
ContactsContainer.AddChild(control);
}
}
else
{
foreach (var group in Groups.Values.OrderBy(g => g.GroupName))
{
var control = new NanoChatGroupControl(group);
control.OnPressed += () => SetActiveChat?.Invoke(group.GroupId);
GroupsContainer.AddChild(control);
}
}
}
@@ -157,6 +199,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
private void UpdateUiState()
{
var hasActiveChat = ActiveChatId != null;
var isGroupChat = hasActiveChat && ActiveChatId?.StartsWith("G") == true;
// Update input state
MessageInput.Editable = hasActiveChat;
@@ -165,13 +208,27 @@ public sealed partial class NanoChatUiFragment : BoxContainer
: Loc.GetString("nano-chat-ui-select-chat-input");
EmojiButton.Disabled = !hasActiveChat;
SendButton.Disabled = !hasActiveChat;
EraseChatButton.Visible = hasActiveChat;
// Update button visibility
EraseChatButton.Visible = hasActiveChat && !isGroupChat;
LeaveChatButton.Visible = hasActiveChat && isGroupChat;
CloseChatButton.Visible = hasActiveChat;
// Update chat title
if (ActiveChatId != null && hasActiveChat && Contacts.TryGetValue(ActiveChatId, out var contact))
if (ActiveChatId != null)
{
ChatTitle.Text = contact.ContactName;
if (isGroupChat && Groups.TryGetValue(ActiveChatId, out var group))
{
ChatTitle.Text = $"{group.GroupName} {ActiveChatId}";
}
else if (!isGroupChat && Contacts.TryGetValue(ActiveChatId, out var contact))
{
ChatTitle.Text = contact.ContactName;
}
else
{
ChatTitle.Text = Loc.GetString("nano-chat-ui-unknown-chat");
}
}
else
{

View File

@@ -379,6 +379,9 @@ namespace Content.Server.Forensics
foreach (var (contactId, messages) in chatComp.Messages)
{
if (contactId.StartsWith("G"))
continue;
if (chatComp.Contacts.TryGetValue(contactId, out var contact))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-with-contact",
@@ -410,6 +413,9 @@ namespace Content.Server.Forensics
foreach (var contact in chatComp.Contacts.Values)
{
if (contact.ContactId.StartsWith("G"))
continue;
if (!chatComp.Messages.ContainsKey(contact.ContactId))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-contact-no-messages",
@@ -418,6 +424,68 @@ namespace Content.Server.Forensics
text.AppendLine();
}
}
if (chatComp.Groups.Count > 0)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-groups-header"));
text.AppendLine(new string('=', 36));
foreach (var (groupId, group) in chatComp.Groups)
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-info",
("name", group.GroupName),
("id", groupId),
("members", group.MemberCount)));
if (chatComp.Messages.TryGetValue(groupId, out var groupMessages))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-messages"));
text.AppendLine(new string('-', 30));
foreach (var message in groupMessages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ?
Loc.GetString("forensic-scanner-chat-you") :
message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
}
else
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-group-no-messages"));
}
text.AppendLine();
}
}
foreach (var (groupId, messages) in chatComp.Messages)
{
if (groupId.StartsWith("G") && !chatComp.Groups.ContainsKey(groupId))
{
text.AppendLine(Loc.GetString("forensic-scanner-chat-archived-group",
("id", groupId)));
text.AppendLine(new string('-', 30));
foreach (var message in messages)
{
var time = $"{(int)message.Timestamp.TotalHours:00}:{message.Timestamp.Minutes:00}";
var sender = message.IsOwnMessage ?
Loc.GetString("forensic-scanner-chat-you") :
message.SenderName;
var status = message.Delivered ? "✓" : "✗";
text.AppendLine($"[{time}] {sender} ({status}): {message.Message}");
}
text.AppendLine();
}
}
}
// Corvax-Wega-NanoChat-end
}

View File

@@ -27,6 +27,7 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
[Dependency] private readonly IRobustRandom _random = default!;
private readonly Dictionary<string, EntityUid> _activeChats = new();
private readonly Dictionary<string, ChatGroupData> _groups = new();
private const int MessageRange = 2000;
public override void Initialize()
@@ -46,7 +47,7 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
SubscribeLocalEvent<NanoChatCartridgeComponent, EmpDisabledRemoved>(OnEmpFinished);
}
private void OnRoundRestart(RoundRestartCleanupEvent args) { _activeChats.Clear(); }
private void OnRoundRestart(RoundRestartCleanupEvent args) { _activeChats.Clear(); _groups.Clear(); }
private void OnOwnerNameChanged(Entity<PdaComponent> ent, ref OwnerNameChangedEvent args)
{
@@ -83,6 +84,17 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
return id;
}
private string GenerateUniqueGroupId()
{
string id;
do
{
id = "G" + _random.Next(10000).ToString("D4");
} while (_groups.ContainsKey(id));
return id;
}
private void OnUiReady(Entity<NanoChatCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
{
UpdateUiState(ent);
@@ -91,6 +103,8 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
private void OnCartridgeRemoved(Entity<NanoChatCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
_activeChats.Remove(ent.Comp.ChatId);
foreach (var group in _groups.Values.Where(g => g.Members.Contains(ent.Comp.ChatId)).ToList())
group.Members.Remove(ent.Comp.ChatId);
}
private void OnEmpPulse(Entity<NanoChatCartridgeComponent> ent, ref EmpPulseEvent args)
@@ -130,6 +144,15 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
case NanoChatSetActiveChat setActiveChat:
SetActiveChat(ent, setActiveChat.ContactId);
break;
case NanoChatCreateGroup createGroup:
CreateGroup(ent, createGroup.GroupName);
break;
case NanoChatJoinGroup joinGroup:
JoinGroup(ent, joinGroup.GroupId);
break;
case NanoChatLeaveGroup leaveGroup:
LeaveGroup(ent, leaveGroup.GroupId);
break;
}
UpdateUiState(ent);
@@ -146,9 +169,7 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
private void EraseContact(Entity<NanoChatCartridgeComponent> ent, string contactId)
{
if (ent.Comp.Contacts.ContainsKey(contactId))
{
ent.Comp.Contacts.Remove(contactId);
}
ent.Comp.ActiveChat = null;
UpdateUiState(ent);
@@ -160,17 +181,86 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
UpdateUiState(ent);
}
private void SetActiveChat(Entity<NanoChatCartridgeComponent> ent, string contactId)
private void SetActiveChat(Entity<NanoChatCartridgeComponent> ent, string chatId)
{
ent.Comp.ActiveChat = contactId;
if (ent.Comp.Contacts.TryGetValue(contactId, out var contact))
ent.Comp.ActiveChat = chatId;
if (ent.Comp.Contacts.TryGetValue(chatId, out var contact))
{
ent.Comp.Contacts[contactId] = new ChatContact(contactId, contact.ContactName, false);
ent.Comp.Contacts[chatId] = new ChatContact(chatId, contact.ContactName, false);
}
if (ent.Comp.Groups.TryGetValue(chatId, out var group))
{
ent.Comp.Groups[chatId] = new ChatGroup(chatId, group.GroupName, false, group.MemberCount);
}
UpdateUiState(ent);
}
private void CreateGroup(Entity<NanoChatCartridgeComponent> ent, string groupName)
{
var groupId = GenerateUniqueGroupId();
var groupData = new ChatGroupData
{
GroupId = groupId,
GroupName = groupName
};
_groups[groupId] = groupData;
var systemMessage = new ChatMessage(
"system", "System",
Loc.GetString("nano-chat-create-group-message", ("name", ent.Comp.OwnerName), ("groupName", groupName)),
_timing.CurTime,
false,
true
);
groupData.Messages.Add(systemMessage);
JoinGroup(ent, groupId);
_admin.Add(LogType.Action, LogImpact.Low,
$"Group created: '{groupName}' (ID: {groupId}) by {ent.Comp.ChatId}");
}
private void JoinGroup(Entity<NanoChatCartridgeComponent> ent, string groupId)
{
if (_groups.TryGetValue(groupId, out var group))
{
group.Members.Add(ent.Comp.ChatId);
ent.Comp.Groups[groupId] = new ChatGroup(
groupId,
group.GroupName,
false,
group.Members.Count
);
NotifyGroupMembers(groupId, Loc.GetString("nano-chat-join-message", ("name", ent.Comp.OwnerName)));
UpdateAllGroupMembersUi(groupId);
}
}
private void LeaveGroup(Entity<NanoChatCartridgeComponent> ent, string groupId)
{
if (_groups.TryGetValue(groupId, out var group))
{
group.Members.Remove(ent.Comp.ChatId);
ent.Comp.Groups.Remove(groupId);
NotifyGroupMembers(groupId, Loc.GetString("nano-chat-leave-message", ("name", ent.Comp.OwnerName)));
if (ent.Comp.ActiveChat == groupId)
{
ent.Comp.ActiveChat = null;
}
UpdateAllGroupMembersUi(groupId);
}
}
private void SendMessage(Entity<NanoChatCartridgeComponent> sender, string recipientId, string message)
{
if (_timing.CurTime < sender.Comp.NextMessageAllowedAfter)
@@ -208,6 +298,12 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
return;
}
if (recipientId.StartsWith("G") && _groups.TryGetValue(recipientId, out var group))
{
SendGroupMessage(sender, group, message, originalMessage);
return;
}
if (!_activeChats.TryGetValue(recipientId, out var recipientEntity))
{
AddUndeliveredMessage(sender, recipientId, originalMessage);
@@ -271,21 +367,107 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
UpdateUiState(sender);
}
private void SendGroupMessage(Entity<NanoChatCartridgeComponent> sender, ChatGroupData group, string message, string originalMessage)
{
var timestamp = _timing.CurTime;
var groupMessage = new ChatMessage(sender.Comp.ChatId, sender.Comp.OwnerName,
message, timestamp, true, true);
group.Messages.Add(groupMessage);
if (!sender.Comp.Messages.ContainsKey(group.GroupId))
sender.Comp.Messages[group.GroupId] = new List<ChatMessage>();
sender.Comp.Messages[group.GroupId].Add(groupMessage);
foreach (var memberId in group.Members)
{
if (memberId == sender.Comp.ChatId)
continue;
if (!_activeChats.TryGetValue(memberId, out var memberEntity))
continue;
if (!TryComp<NanoChatCartridgeComponent>(memberEntity, out var memberComp))
continue;
if (!IsWithinRange(sender.Owner, memberEntity))
continue;
if (!memberComp.Messages.ContainsKey(group.GroupId))
memberComp.Messages[group.GroupId] = new List<ChatMessage>();
var memberMessage = new ChatMessage(sender.Comp.ChatId, sender.Comp.OwnerName,
message, timestamp, false, true);
memberComp.Messages[group.GroupId].Add(memberMessage);
if (memberComp.Groups.TryGetValue(group.GroupId, out var memberGroup))
{
memberComp.Groups[group.GroupId] = new ChatGroup(
memberGroup.GroupId,
memberGroup.GroupName,
true,
group.Members.Count
);
}
if (TryComp<CartridgeComponent>(memberEntity, out var cartridge)
&& cartridge.LoaderUid.HasValue && !memberComp.MutedSound)
_audio.PlayPvs(memberComp.Sound, memberEntity);
UpdateUiState((memberEntity, memberComp));
}
_admin.Add(LogType.Action, LogImpact.Low,
$"Group message: '{originalMessage}' by {sender.Comp.ChatId} in group {group.GroupId}.");
UpdateUiState(sender);
}
private void UpdateUiState(Entity<NanoChatCartridgeComponent> ent)
{
List<ChatMessage>? activeMessages = null;
if (ent.Comp.ActiveChat != null && ent.Comp.Messages.TryGetValue(ent.Comp.ActiveChat, out var messages))
if (ent.Comp.ActiveChat != null)
{
activeMessages = messages;
if (ent.Comp.ActiveChat.StartsWith("G") && _groups.TryGetValue(ent.Comp.ActiveChat, out var group))
{
activeMessages = group.Messages;
}
else if (ent.Comp.Messages.TryGetValue(ent.Comp.ActiveChat, out var messages))
{
activeMessages = messages;
}
}
if (!TryComp<CartridgeComponent>(ent, out var cartridge) || !cartridge.LoaderUid.HasValue)
return;
var state = new NanoChatUiState(ent.Comp.ChatId, ent.Comp.ActiveChat, ent.Comp.MutedSound, ent.Comp.Contacts, activeMessages);
var state = new NanoChatUiState(ent.Comp.ChatId, ent.Comp.ActiveChat, ent.Comp.MutedSound, ent.Comp.Contacts, ent.Comp.Groups, activeMessages);
_cartridgeLoader.UpdateCartridgeUiState(cartridge.LoaderUid.Value, state);
}
private void UpdateAllGroupMembersUi(string groupId)
{
if (_groups.TryGetValue(groupId, out var group))
{
foreach (var memberId in group.Members)
{
if (_activeChats.TryGetValue(memberId, out var memberEntity) &&
TryComp<NanoChatCartridgeComponent>(memberEntity, out var memberComp))
{
memberComp.Groups[groupId] = new ChatGroup(
groupId,
group.GroupName,
memberComp.Groups.TryGetValue(groupId, out var currentGroup) ? currentGroup.HasUnread : false,
group.Members.Count
);
UpdateUiState((memberEntity, memberComp));
}
}
}
}
private bool IsWithinRange(EntityUid sender, EntityUid recipient)
{
if (!Transform(sender).Coordinates.IsValid(EntityManager) ||
@@ -300,6 +482,27 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
return senderCoords.InRange(recipientCoords, MessageRange);
}
private void NotifyGroupMembers(string groupId, string message)
{
if (_groups.TryGetValue(groupId, out var group))
{
var timestamp = _timing.CurTime;
var groupMessage = new ChatMessage("system", "System", message, timestamp, false, true);
group.Messages.Add(groupMessage);
foreach (var memberId in group.Members)
{
if (_activeChats.TryGetValue(memberId, out var memberEntity) &&
TryComp<NanoChatCartridgeComponent>(memberEntity, out var memberComp) &&
TryComp<CartridgeComponent>(memberEntity, out var cartridge) &&
cartridge.LoaderUid.HasValue && !memberComp.MutedSound)
{
_audio.PlayPvs(memberComp.Sound, memberEntity);
}
}
}
}
private void AddUndeliveredMessage(Entity<NanoChatCartridgeComponent> sender, string recipientId, string message)
{
var timestamp = _timing.CurTime;
@@ -375,6 +578,19 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
contact.HasUnread
);
}
foreach (var groupKey in comp.Groups.Keys.ToList())
{
var group = comp.Groups[groupKey];
var distortedName = DistortMessage(group.GroupName);
comp.Groups[groupKey] = new ChatGroup(
group.GroupId,
distortedName,
group.HasUnread,
group.MemberCount
);
}
}
private string DistortMessage(string originalMessage)
@@ -400,4 +616,12 @@ public sealed class NanoChatCartridgeSystem : SharedNanoChatCartridgeSystem
return new string(result);
}
private sealed class ChatGroupData
{
public string GroupId = string.Empty;
public string GroupName = string.Empty;
public HashSet<string> Members = new();
public List<ChatMessage> Messages = new();
}
}

View File

@@ -3,6 +3,7 @@ using Robust.Shared.Audio;
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, AutoGenerateComponentPause]
// [Access(typeof(SharedNanoChatCartridgeSystem))]
public sealed partial class NanoChatCartridgeComponent : Component
{
[DataField]
@@ -26,6 +27,9 @@ public sealed partial class NanoChatCartridgeComponent : Component
[DataField]
public Dictionary<string, List<ChatMessage>> Messages = new();
[DataField]
public Dictionary<string, ChatGroup> Groups = new();
[DataField, AutoPausedField]
public TimeSpan NextMessageAllowedAfter = TimeSpan.Zero;

View File

@@ -57,6 +57,39 @@ public sealed class NanoChatSetActiveChat : INanoChatUiMessagePayload
}
}
[Serializable, NetSerializable]
public sealed class NanoChatCreateGroup : INanoChatUiMessagePayload
{
public string GroupName { get; }
public NanoChatCreateGroup(string groupName)
{
GroupName = groupName;
}
}
[Serializable, NetSerializable]
public sealed class NanoChatJoinGroup : INanoChatUiMessagePayload
{
public string GroupId { get; }
public NanoChatJoinGroup(string groupId)
{
GroupId = groupId;
}
}
[Serializable, NetSerializable]
public sealed class NanoChatLeaveGroup : INanoChatUiMessagePayload
{
public string GroupId { get; }
public NanoChatLeaveGroup(string groupId)
{
GroupId = groupId;
}
}
[Serializable, NetSerializable]
public sealed class NanoChatUiMessageEvent : CartridgeMessageEvent
{

View File

@@ -9,14 +9,21 @@ public sealed class NanoChatUiState : BoundUserInterfaceState
public string? ActiveChat;
public bool Muted;
public Dictionary<string, ChatContact> Contacts;
public Dictionary<string, ChatGroup> Groups;
public List<ChatMessage>? ActiveChatMessages;
public NanoChatUiState(string chatId, string? activeChat, bool muted, Dictionary<string, ChatContact> contacts, List<ChatMessage>? activeChatMessages)
public NanoChatUiState(
string chatId, string? activeChat, bool muted,
Dictionary<string, ChatContact> contacts,
Dictionary<string, ChatGroup> groups,
List<ChatMessage>? activeChatMessages
)
{
ChatId = chatId;
ActiveChat = activeChat;
Muted = muted;
Contacts = contacts;
Groups = groups;
ActiveChatMessages = activeChatMessages;
}
}
@@ -36,6 +43,23 @@ public sealed class ChatContact
}
}
[Serializable, NetSerializable]
public sealed class ChatGroup
{
public string GroupId { get; }
public string GroupName { get; }
public bool HasUnread { get; }
public int MemberCount { get; }
public ChatGroup(string groupId, string groupName, bool hasUnread, int memberCount)
{
GroupId = groupId;
GroupName = groupName;
HasUnread = hasUnread;
MemberCount = memberCount;
}
}
[Serializable, NetSerializable]
public sealed class ChatMessage
{

View File

@@ -18,4 +18,10 @@ forensic-scanner-chat-with-contact = Чат с: { $name } ({ $id })
forensic-scanner-chat-with-unknown = Чат с: Неизвестный ({ $id })
forensic-scanner-chat-contact-no-messages = Контакт: { $name } ({ $id }) - нет сообщений
forensic-scanner-chat-groups-header = ГРУППОВЫЕ ЧАТЫ
forensic-scanner-chat-group-info = Группа: { $name } (ID: { $id }, Участников: { $members })
forensic-scanner-chat-group-messages = Сообщения группы:
forensic-scanner-chat-group-no-messages = Нет сообщений в группе
forensic-scanner-chat-archived-group = Архивная группа: { $id }
forensic-scanner-chat-you = Вы

View File

@@ -1,22 +1,60 @@
# Ui
nano-chat-ui-add = Добавить
nano-chat-ui-add-contact-title = Добавить контакт
nano-chat-ui-add-contact-tooltip = Добавить контакт
nano-chat-ui-cancel = Отмена
nano-chat-ui-close-chat-tooltip = Закрыть чат
nano-chat-ui-contact-id = ID контакта
nano-chat-ui-contact-id-placeholder = Введите ID контакта(5 символов)...
nano-chat-ui-contact-name = Имя контакта
nano-chat-ui-contact-name-placeholder = Введите имя контакта(9 символов)...
nano-chat-ui-cancel = Отмена
nano-chat-ui-add = Добавить
nano-chat-ui-emoji-title = Выбор эмодзи
nano-chat-ui-create = Создать
nano-chat-ui-create-group-title = Создать группу
nano-chat-ui-create-group-tooltip = Создать новую группу
nano-chat-ui-emoji-select = Выберите эмодзи:
nano-chat-ui-emoji-title = Выбор эмодзи
nano-chat-ui-emoji-tooltip = Эмодзи
nano-chat-ui-unread-indicator = (У)
nano-chat-ui-title = NanoChat
nano-chat-ui-mute-tooltip = Отключить звук
nano-chat-ui-unmute-tooltip = Включить звук
nano-chat-ui-add-contact-tooltip = Добавить контакт
nano-chat-ui-no-contacts = Нет контактов
nano-chat-ui-erase-chat-tooltip = Стереть контакт
nano-chat-ui-close-chat-tooltip = Закрыть чат
nano-chat-ui-group-id = ID группы
nano-chat-ui-group-id-placeholder = Введите ID группы(5 символов)...
nano-chat-ui-group-name = Название группы
nano-chat-ui-group-name-placeholder = Введите название группы(16 символов)...
nano-chat-ui-join = Присоединиться
nano-chat-ui-join-group-title = Присоединиться к группе
nano-chat-ui-join-group-tooltip = Присоединиться к группе
nano-chat-ui-leave-chat-tooltip = Покинуть группу
nano-chat-ui-message-placeholder = Введите сообщение...
nano-chat-ui-mute-tooltip = Отключить звук
nano-chat-ui-no-contacts = Нет контактов
nano-chat-ui-no-groups = Нет групп
nano-chat-ui-select-chat = Выберите чат
nano-chat-ui-select-chat-input = Выберите чат для сообщения
nano-chat-ui-message-placeholder = Введите сообщение...
nano-chat-ui-send-tooltip = Отправить
nano-chat-ui-tab-contacts = Конт.
nano-chat-ui-tab-groups = Групп.
nano-chat-ui-title = NanoChat
nano-chat-ui-unmute-tooltip = Включить звук
nano-chat-ui-unread-indicator = (У)
# System
nano-chat-create-group-message = {$name} создал группу '{$groupName}'.
nano-chat-join-message = -> {$name} присоединился!
nano-chat-leave-message = <- {$name} покинул группу.