Доработка мессенжера

This commit is contained in:
Vigers Ray 2026-01-28 00:20:07 +01:00
parent d4989a6645
commit 1fabbbb504
19 changed files with 1526 additions and 84 deletions

View file

@ -1,5 +1,6 @@
using Content.Client._Sunrise.Messenger;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared._Sunrise.Messenger;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
@ -21,6 +22,11 @@ public sealed partial class MessagePanel : PanelContainer
private ClientEmojiSystem? EmojiSystem => _entitySystemManager.GetEntitySystemOrNull<ClientEmojiSystem>();
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
/// <summary>
/// ID сообщения для идентификации при обновлении
/// </summary>
public long MessageId { get; private set; }
private static readonly Type[] MessageTagsAllowed =
[
typeof(BoldItalicTag),
@ -38,14 +44,38 @@ public sealed partial class MessagePanel : PanelContainer
RobustXamlLoader.Load(this);
}
public event Action<long>? OnDeleteMessage;
private Button? _deleteButton;
public void UpdateMessage(MessengerMessage message, bool isOwnMessage, bool isPersonalChat, string? currentUserId)
{
MessageId = message.MessageId;
SenderLabel.Text = message.SenderName;
TimeLabel.Text = message.Timestamp.ToString(@"hh\:mm");
var parsedContent = EmojiSystem?.ParseEmojis(message.Content) ?? message.Content;
ContentLabel.SetMessage(FormattedMessage.FromMarkupPermissive(parsedContent), MessageTagsAllowed);
if (isOwnMessage && _deleteButton == null)
{
_deleteButton = new Button
{
Text = "✖",
SetWidth = 24,
SetHeight = 24,
ToolTip = Loc.GetString("messenger-delete-message"),
Margin = new Thickness(2, 0)
};
_deleteButton.AddStyleClass(StyleClass.ButtonSmall);
_deleteButton.OnPressed += _ => OnDeleteMessage?.Invoke(MessageId);
RightContainer.AddChild(_deleteButton);
}
else if (!isOwnMessage && _deleteButton != null)
{
RightContainer.RemoveChild(_deleteButton);
_deleteButton = null;
}
var roundedButtonTex = _resourceCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
var roundedStyleBox = new StyleBoxTexture
{

View file

@ -29,6 +29,14 @@ public sealed partial class MessengerUi : UIFragment
SendMessengerMessage(MessengerUiAction.RequestMessages, userInterface, chatId: chatId);
_fragment.OnToggleMute += (chatId, isMuted) =>
SendMessengerMessage(MessengerUiAction.ToggleMute, userInterface, chatId: chatId, isMuted: isMuted);
_fragment.OnAcceptInvite += (groupId) =>
SendMessengerMessage(MessengerUiAction.AcceptInvite, userInterface, groupId: groupId);
_fragment.OnDeclineInvite += (groupId) =>
SendMessengerMessage(MessengerUiAction.DeclineInvite, userInterface, groupId: groupId);
_fragment.OnLeaveGroup += (groupId) =>
SendMessengerMessage(MessengerUiAction.LeaveGroup, userInterface, groupId: groupId);
_fragment.OnDeleteMessage += (chatId, messageId) =>
SendMessengerMessage(MessengerUiAction.DeleteMessage, userInterface, chatId: chatId, messageId: messageId);
}
public override void UpdateState(BoundUserInterfaceState state)
@ -48,9 +56,10 @@ public sealed partial class MessengerUi : UIFragment
string? groupName = null,
string? userId = null,
string? chatId = null,
bool? isMuted = null)
bool? isMuted = null,
long? messageId = null)
{
var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted);
var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted, messageId);
var message = new CartridgeUiMessage(messengerMessage);
userInterface.SendMessage(message);
}

View file

@ -9,7 +9,7 @@
VerticalExpand="True">
<BoxContainer
Orientation="Vertical"
SetWidth="200"
SetWidth="220"
VerticalExpand="True">
<BoxContainer
HorizontalExpand="True"
@ -30,16 +30,27 @@
PlaceHolder="{Loc 'messenger-search-placeholder'}" />
<BoxContainer
HorizontalExpand="True"
Orientation="Horizontal"
Orientation="Vertical"
Margin="4,2">
<Button
Name="PersonalChatsTab"
Text="{Loc 'messenger-tab-personal'}"
<BoxContainer
HorizontalExpand="True"
ToggleMode="True" />
Orientation="Horizontal">
<Button
Name="PersonalChatsTab"
Text="{Loc 'messenger-tab-personal'}"
HorizontalExpand="True"
SizeFlagsStretchRatio="1"
ToggleMode="True" />
<Button
Name="GroupChatsTab"
Text="{Loc 'messenger-tab-groups'}"
HorizontalExpand="True"
SizeFlagsStretchRatio="1"
ToggleMode="True" />
</BoxContainer>
<Button
Name="GroupChatsTab"
Text="{Loc 'messenger-tab-groups'}"
Name="InvitesTab"
Text="{Loc 'messenger-tab-invites'}"
HorizontalExpand="True"
ToggleMode="True" />
</BoxContainer>

View file

@ -29,6 +29,10 @@ public sealed partial class MessengerUiFragment : BoxContainer
public event Action<string, string>? OnRemoveFromGroup;
public event Action<string>? OnRequestMessages;
public event Action<string, bool>? OnToggleMute;
public event Action<string>? OnAcceptInvite;
public event Action<string>? OnDeclineInvite;
public event Action<string>? OnLeaveGroup;
public event Action<string, long>? OnDeleteMessage;
private string? _currentChatId;
private MessengerUiState? _currentState;
@ -40,6 +44,11 @@ public sealed partial class MessengerUiFragment : BoxContainer
private int _lastMessageCount;
private bool _isScrolledToBottom = true;
/// <summary>
/// Максимальное количество сообщений, отображаемых в UI (для оптимизации производительности)
/// </summary>
private const int MaxDisplayedMessages = 100;
private Dictionary<string, int>? _lastUnreadCounts;
private HashSet<string>? _lastUserIds;
private HashSet<string>? _lastGroupIds;
@ -88,6 +97,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
SearchInput.OnTextChanged += OnSearchTextChanged;
PersonalChatsTab.OnPressed += _ => SwitchTab(0);
GroupChatsTab.OnPressed += _ => SwitchTab(1);
InvitesTab.OnPressed += _ => SwitchTab(2);
MuteCheckBox.OnToggled += OnMuteToggled;
PersonalChatsTab.Pressed = true;
@ -207,6 +217,11 @@ public sealed partial class MessengerUiFragment : BoxContainer
.ThenBy(m => m.Content)
.ToList();
if (messages.Count > MaxDisplayedMessages)
{
messages = messages.Skip(messages.Count - MaxDisplayedMessages).ToList();
}
var wasChatChanged = savedChatId != _currentChatId;
if (wasChatChanged)
@ -288,6 +303,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
}
UpdateChatsList(state);
UpdateTabButtons(state);
if (_showMembersList && _currentGroupId != null)
{
@ -295,15 +311,158 @@ public sealed partial class MessengerUiFragment : BoxContainer
}
}
private void UpdateTabButtons(MessengerUiState state)
{
int personalUnreadChats = 0;
int groupsUnreadChats = 0;
int invitesCount = 0;
if (state.CurrentUserId != null)
{
foreach (var user in state.Users)
{
if (user.UserId == state.CurrentUserId)
continue;
var chatId = GetPersonalChatId(user.UserId);
if (state.UnreadCounts.TryGetValue(chatId, out var count) && count > 0)
{
personalUnreadChats++;
}
}
}
foreach (var group in state.Groups)
{
if (state.UnreadCounts.TryGetValue(group.GroupId, out var count) && count > 0)
{
groupsUnreadChats++;
}
}
if (state.ActiveInvites != null)
{
invitesCount = state.ActiveInvites.Count;
}
UpdateTabButton(PersonalChatsTab, personalUnreadChats);
UpdateTabButton(GroupChatsTab, groupsUnreadChats);
UpdateTabButton(InvitesTab, invitesCount);
}
private void UpdateTabButton(Button button, int count)
{
BoxContainer? textContainer;
Label? nameLabel;
var isInvitesTab = button == InvitesTab;
if (button.ChildCount == 0)
{
textContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
nameLabel = new Label
{
Text = button.Text ?? "",
HorizontalExpand = false,
Align = isInvitesTab ? Label.AlignMode.Center : Label.AlignMode.Left
};
textContainer.AddChild(nameLabel);
button.AddChild(textContainer);
}
else
{
textContainer = button.Children.FirstOrDefault() as BoxContainer;
if (textContainer == null)
{
textContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
nameLabel = new Label
{
Text = button.Text ?? "",
HorizontalExpand = false,
Align = isInvitesTab ? Label.AlignMode.Center : Label.AlignMode.Left
};
textContainer.AddChild(nameLabel);
button.RemoveAllChildren();
button.AddChild(textContainer);
}
else
{
nameLabel = textContainer.Children.OfType<Label>().FirstOrDefault(l => !l.StyleClasses.Contains("Bold"));
if (nameLabel == null)
{
nameLabel = new Label
{
Text = button.Text ?? "",
HorizontalExpand = false,
Align = isInvitesTab ? Label.AlignMode.Center : Label.AlignMode.Left
};
textContainer.AddChild(nameLabel);
}
else
{
nameLabel.HorizontalExpand = false;
nameLabel.Align = isInvitesTab ? Label.AlignMode.Center : Label.AlignMode.Left;
}
}
}
if (textContainer == null || nameLabel == null)
return;
var existingCounter = textContainer.Children.OfType<Label>()
.FirstOrDefault(l => l != nameLabel && l.StyleClasses.Contains("Bold"));
if (count > 0)
{
button.ModulateSelfOverride = Color.Red;
if (existingCounter == null)
{
var counterLabel = new Label
{
Text = $"({count})",
Align = Label.AlignMode.Left,
StyleClasses = { "Bold" },
HorizontalExpand = false
};
counterLabel.ModulateSelfOverride = Color.White;
textContainer.AddChild(counterLabel);
}
else
{
existingCounter.Text = $"({count})";
}
}
else
{
button.ModulateSelfOverride = null;
if (existingCounter != null)
{
textContainer.RemoveChild(existingCounter);
}
}
}
private void SwitchTab(int tabIndex)
{
_activeTab = tabIndex;
PersonalChatsTab.Pressed = tabIndex == 0;
GroupChatsTab.Pressed = tabIndex == 1;
InvitesTab.Pressed = tabIndex == 2;
CreateGroupButton.Visible = tabIndex == 1;
if (_currentState != null)
{
UpdateChatsList(_currentState);
UpdateTabButtons(_currentState);
}
}
@ -444,7 +603,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
ChatsContainer.AddChild(buttonContainer);
}
}
else
else if (_activeTab == 1)
{
foreach (var group in state.Groups)
{
@ -510,6 +669,94 @@ public sealed partial class MessengerUiFragment : BoxContainer
ChatsContainer.AddChild(groupContainer);
}
}
else if (_activeTab == 2)
{
if (state.ActiveInvites.Count > 0)
{
foreach (var invite in state.ActiveInvites)
{
var invitePanel = new PanelContainer
{
HorizontalExpand = true,
Margin = new Thickness(2, 2)
};
invitePanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#25252A"),
BorderColor = Color.FromHex("#3D4059"),
BorderThickness = new Thickness(1),
ContentMarginLeftOverride = 6,
ContentMarginRightOverride = 6,
ContentMarginTopOverride = 4,
ContentMarginBottomOverride = 4
};
var inviteContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true
};
var groupNameLabel = new Label
{
Text = invite.GroupName,
StyleClasses = { "Bold" },
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 4),
ClipText = true
};
inviteContainer.AddChild(groupNameLabel);
var inviteInfo = new RichTextLabel
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 8)
};
inviteInfo.SetMessage(Loc.GetString("messenger-invite-info", ("inviter", invite.InviterName)), tagsAllowed: null);
inviteInfo.StyleClasses.Add("LabelSubText");
inviteContainer.AddChild(inviteInfo);
var buttonsContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
var acceptButton = new Button
{
Text = "✓",
HorizontalExpand = true,
Margin = new Thickness(2, 0)
};
acceptButton.OnPressed += _ => OnAcceptInvite?.Invoke(invite.GroupId);
buttonsContainer.AddChild(acceptButton);
var declineButton = new Button
{
Text = "✖",
HorizontalExpand = true,
Margin = new Thickness(2, 0)
};
declineButton.OnPressed += _ => OnDeclineInvite?.Invoke(invite.GroupId);
buttonsContainer.AddChild(declineButton);
inviteContainer.AddChild(buttonsContainer);
invitePanel.AddChild(inviteContainer);
ChatsContainer.AddChild(invitePanel);
}
}
else
{
var noInvitesLabel = new Label
{
Text = Loc.GetString("messenger-no-invites"),
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(4)
};
ChatsContainer.AddChild(noInvitesLabel);
}
}
}
private bool AreUnreadCountsEqual(Dictionary<string, int> counts1, Dictionary<string, int> counts2)
@ -789,31 +1036,86 @@ public sealed partial class MessengerUiFragment : BoxContainer
MembersList.AddChild(noMembersLabel);
}
if (isUserGroup && isOwner && _currentState != null && _currentState.ServerAvailable && _currentState.IsRegistered)
if (isUserGroup && _currentState != null && _currentState.ServerAvailable && _currentState.IsRegistered)
{
AddMemberButton.Disabled = false;
AddMemberButton.Visible = true;
AddMemberButton.OnPressed -= OnAddMemberButtonPressed;
AddMemberButton.OnPressed += OnAddMemberButtonPressed;
var buttonsContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Margin = new Thickness(2, 4)
};
if (group.Type == MessengerGroupType.UserCreated && isOwner)
{
if (AddMemberButton.Parent != null)
{
AddMemberButton.Parent.RemoveChild(AddMemberButton);
}
AddMemberButton.Disabled = false;
AddMemberButton.Visible = true;
AddMemberButton.Text = Loc.GetString("messenger-invite-member-button");
AddMemberButton.HorizontalExpand = true;
AddMemberButton.Margin = new Thickness(2, 0);
AddMemberButton.OnPressed -= OnAddMemberButtonPressed;
AddMemberButton.OnPressed += OnAddMemberButtonPressed;
buttonsContainer.AddChild(AddMemberButton);
}
else
{
AddMemberButton.Disabled = true;
AddMemberButton.Visible = false;
}
var leaveButton = new Button
{
Text = Loc.GetString("messenger-leave-group"),
HorizontalExpand = true,
Margin = new Thickness(2, 0)
};
leaveButton.OnPressed += _ =>
{
if (_currentGroupId != null)
{
OnLeaveGroup?.Invoke(_currentGroupId);
}
};
buttonsContainer.AddChild(leaveButton);
MembersList.AddChild(buttonsContainer);
}
else
{
AddMemberButton.Disabled = true;
AddMemberButton.Visible = false;
}
}
private void UpdateMessages(List<MessengerMessage> messages)
{
if (messages.Count > MaxDisplayedMessages)
{
messages = messages.Skip(messages.Count - MaxDisplayedMessages).ToList();
}
var hasNewMessages = messages.Count > _lastMessageCount;
UpdateScrollBottomState();
var wasAtBottom = _isScrolledToBottom;
if (_lastMessageCount == 0)
if (_lastMessageCount == 0 || messages.Count < _lastMessageCount)
{
MessagesList.RemoveAllChildren();
_lastMessageCount = 0;
}
else if (messages.Count > 0 && _lastMessageCount > 0)
{
var firstDisplayedId = GetFirstDisplayedMessageId();
if (firstDisplayedId.HasValue && messages[0].MessageId != firstDisplayedId.Value)
{
MessagesList.RemoveAllChildren();
_lastMessageCount = 0;
}
}
var hasStatusChange = false;
@ -858,17 +1160,53 @@ public sealed partial class MessengerUiFragment : BoxContainer
_lastMessageCount = 0;
}
var existingMessageIds = new HashSet<long>();
foreach (var child in MessagesList.Children)
{
if (child is MessagePanel panel)
{
existingMessageIds.Add(panel.MessageId);
}
}
for (int i = _lastMessageCount; i < messages.Count; i++)
{
var message = messages[i];
if (existingMessageIds.Contains(message.MessageId))
continue;
var isOwnMessage = _currentState?.CurrentUserId == message.SenderId;
var isPersonalChat = _currentChatId != null && _currentChatId.StartsWith("personal_");
var messagePanel = new MessagePanel();
messagePanel.UpdateMessage(message, isOwnMessage, isPersonalChat, _currentState?.CurrentUserId);
if (isOwnMessage)
{
messagePanel.OnDeleteMessage += (messageId) =>
{
if (_currentChatId != null)
{
OnDeleteMessage?.Invoke(_currentChatId, messageId);
}
};
}
MessagesList.AddChild(messagePanel);
}
var messageIds = new HashSet<long>(messages.Select(m => m.MessageId));
var toRemove = new List<Control>();
foreach (var child in MessagesList.Children)
{
if (child is MessagePanel panel && !messageIds.Contains(panel.MessageId))
{
toRemove.Add(child);
}
}
foreach (var child in toRemove)
{
MessagesList.RemoveChild(child);
}
var previousMessageCount = _lastMessageCount;
_lastMessageCount = messages.Count;
@ -895,6 +1233,21 @@ public sealed partial class MessengerUiFragment : BoxContainer
UpdateScrollBottomState();
}
/// <summary>
/// Получает ID первого отображаемого сообщения для проверки необходимости полной перерисовки
/// </summary>
private long? GetFirstDisplayedMessageId()
{
if (MessagesList.ChildCount == 0)
return null;
var firstChild = MessagesList.Children.FirstOrDefault();
if (firstChild is not MessagePanel firstPanel)
return null;
return firstPanel.MessageId;
}
private void SendMessage()
{
var messageText = MessageInput.Text;
@ -1023,7 +1376,8 @@ public sealed partial class MessengerUiFragment : BoxContainer
}
var dialog = new AddUserDialog();
dialog.SetTitle(Loc.GetString("messenger-add-user-to-group-title", ("groupName", groupName)));
var dialogTitle = Loc.GetString("messenger-invite-user-to-group-title", ("groupName", groupName));
dialog.SetTitle(dialogTitle);
_addUserDialog = dialog;
dialog.SetAvailableUsers(availableUsers);

View file

@ -97,4 +97,10 @@ public sealed partial class MessengerCartridgeComponent : Component
/// </summary>
[ViewVariables]
public Dictionary<string, int> ServerUnreadCounts = new();
/// <summary>
/// Активные приглашения в группы
/// </summary>
[ViewVariables]
public List<MessengerGroupInvite> ActiveInvites = new();
}

View file

@ -52,6 +52,22 @@ public sealed partial class MessengerCartridgeSystem
if (message.ChatId != null && message.IsMuted.HasValue)
ToggleMute(uid, component, message.ChatId, message.IsMuted.Value);
break;
case MessengerUiAction.AcceptInvite:
if (message.GroupId != null)
AcceptInvite(uid, component, loaderUid, deviceNetwork, message.GroupId);
break;
case MessengerUiAction.DeclineInvite:
if (message.GroupId != null)
DeclineInvite(uid, component, loaderUid, deviceNetwork, message.GroupId);
break;
case MessengerUiAction.LeaveGroup:
if (message.GroupId != null)
LeaveGroup(uid, component, loaderUid, deviceNetwork, message.GroupId);
break;
case MessengerUiAction.DeleteMessage:
if (message.ChatId != null && message.MessageId.HasValue)
DeleteMessage(uid, component, loaderUid, deviceNetwork, message.ChatId, message.MessageId.Value);
break;
}
}
@ -307,4 +323,149 @@ public sealed partial class MessengerCartridgeSystem
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
}
private void AcceptInvite(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupId)
{
if (component.ServerAddress == null || !component.IsRegistered)
return;
var messengerFreq = GetMessengerFrequency();
if (!messengerFreq.HasValue)
return;
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
{
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
return;
}
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
{
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdAcceptInvite,
[MessengerCommands.CmdAcceptInvite] = new NetworkPayload
{
["group_id"] = groupId
}
};
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
}
private void DeclineInvite(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupId)
{
if (component.ServerAddress == null || !component.IsRegistered)
return;
var messengerFreq = GetMessengerFrequency();
if (!messengerFreq.HasValue)
return;
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
{
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
return;
}
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
{
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdDeclineInvite,
[MessengerCommands.CmdDeclineInvite] = new NetworkPayload
{
["group_id"] = groupId
}
};
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
}
private void LeaveGroup(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupId)
{
if (component.ServerAddress == null || !component.IsRegistered)
return;
var messengerFreq = GetMessengerFrequency();
if (!messengerFreq.HasValue)
return;
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
{
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
return;
}
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
{
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdLeaveGroup,
[MessengerCommands.CmdLeaveGroup] = new NetworkPayload
{
["group_id"] = groupId
}
};
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
}
private void DeleteMessage(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string chatId, long messageId)
{
if (component.ServerAddress == null || !component.IsRegistered)
return;
var messengerFreq = GetMessengerFrequency();
if (!messengerFreq.HasValue)
return;
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
{
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
return;
}
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
{
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
return;
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdDeleteMessage,
[MessengerCommands.CmdDeleteMessage] = new NetworkPayload
{
["message_id"] = messageId,
["chat_id"] = chatId
}
};
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
}
}

View file

@ -53,6 +53,21 @@ public sealed partial class MessengerCartridgeSystem
case MessengerCommands.CmdUserAddedToGroup:
HandleUserAddedToGroup(uid, component, packet, loaderUid);
break;
case MessengerCommands.CmdInviteReceived:
HandleInviteReceived(uid, component, packet, loaderUid);
break;
case MessengerCommands.CmdInvitesList:
HandleInvitesList(uid, component, packet, loaderUid);
break;
case MessengerCommands.CmdUserLeftGroup:
HandleUserLeftGroup(uid, component, packet, loaderUid);
break;
case MessengerCommands.CmdGroupDeleted:
HandleGroupDeleted(uid, component, packet, loaderUid);
break;
case MessengerCommands.CmdMessageDeleted:
HandleMessageDeleted(uid, component, packet, loaderUid);
break;
default:
Sawmill.Warning($"Unknown command received: {command}");
break;
@ -181,6 +196,17 @@ public sealed partial class MessengerCartridgeSystem
component.Groups = groups;
foreach (var group in groups)
{
if (group.Type == MessengerGroupType.Automatic && group.AutoGroupPrototypeId != null)
{
if (!component.MutedGroupChats.Contains(group.GroupId))
{
component.MutedGroupChats.Add(group.GroupId);
}
}
}
if (serverUnreadCounts != null)
{
foreach (var (chatId, count) in serverUnreadCounts)
@ -494,6 +520,82 @@ public sealed partial class MessengerCartridgeSystem
}
}
private void HandleInviteReceived(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (!packet.Data.TryGetValue("group_id", out object? groupIdObj) ||
!packet.Data.TryGetValue("group_name", out object? groupNameObj) ||
!packet.Data.TryGetValue("inviter_id", out object? inviterIdObj) ||
!packet.Data.TryGetValue("inviter_name", out object? inviterNameObj) ||
!packet.Data.TryGetValue("created_at", out object? createdAtObj))
return;
var groupId = groupIdObj?.ToString();
var groupName = groupNameObj?.ToString();
var inviterId = inviterIdObj?.ToString();
var inviterName = inviterNameObj?.ToString();
if (groupId == null || groupName == null || inviterId == null || inviterName == null)
return;
if (!double.TryParse(createdAtObj?.ToString(), out var createdAtSeconds))
return;
var createdAt = TimeSpan.FromSeconds(createdAtSeconds);
var invite = new MessengerGroupInvite(groupId, groupName, inviterId, inviterName, component.UserId ?? string.Empty, createdAt);
var isNewInvite = !component.ActiveInvites.Any(inv => inv.GroupId == groupId && inv.InviterId == inviterId);
if (isNewInvite)
{
component.ActiveInvites.Add(invite);
// Воспроизводим рингтон при получении нового инвайта
if (TryComp<RingerComponent>(loaderUid, out var ringer))
{
_ringer.RingerPlayRingtone(loaderUid);
}
}
UpdateUiState(uid, loaderUid, component);
}
private void HandleInvitesList(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (!packet.Data.TryGetValue("invites", out List<Dictionary<string, object>>? invitesData))
return;
component.ActiveInvites.Clear();
foreach (var inviteData in invitesData)
{
if (!inviteData.TryGetValue("group_id", out object? groupIdObj) ||
!inviteData.TryGetValue("group_name", out object? groupNameObj) ||
!inviteData.TryGetValue("inviter_id", out object? inviterIdObj) ||
!inviteData.TryGetValue("inviter_name", out object? inviterNameObj) ||
!inviteData.TryGetValue("created_at", out object? createdAtObj))
continue;
var groupId = groupIdObj?.ToString();
var groupName = groupNameObj?.ToString();
var inviterId = inviterIdObj?.ToString();
var inviterName = inviterNameObj?.ToString();
if (groupId == null || groupName == null || inviterId == null || inviterName == null)
continue;
if (!double.TryParse(createdAtObj?.ToString(), out var createdAtSeconds))
continue;
var createdAt = TimeSpan.FromSeconds(createdAtSeconds);
var invite = new MessengerGroupInvite(groupId, groupName, inviterId, inviterName, component.UserId ?? string.Empty, createdAt);
component.ActiveInvites.Add(invite);
}
UpdateUiState(uid, loaderUid, component);
}
private void HandleGroupCreated(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
@ -520,4 +622,83 @@ public sealed partial class MessengerCartridgeSystem
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
RequestGroups(uid, component, loaderUid, deviceNetwork);
}
private void HandleUserLeftGroup(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (!packet.Data.TryGetValue("group_id", out object? groupIdObj) ||
!packet.Data.TryGetValue("user_id", out object? userIdObj))
return;
var groupId = groupIdObj?.ToString();
var userId = userIdObj?.ToString();
if (string.IsNullOrEmpty(groupId))
return;
if (userId == component.UserId)
{
component.Groups.RemoveAll(g => g.GroupId == groupId);
component.MessageHistory.Remove(groupId);
component.MutedGroupChats.Remove(groupId);
component.ServerUnreadCounts.Remove(groupId);
}
else
{
var group = component.Groups.FirstOrDefault(g => g.GroupId == groupId);
if (group != null && !string.IsNullOrEmpty(userId))
{
group.Members.Remove(userId);
}
}
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
RequestGroups(uid, component, loaderUid, deviceNetwork);
UpdateUiState(uid, loaderUid, component);
}
private void HandleGroupDeleted(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (!packet.Data.TryGetValue("group_id", out object? groupIdObj))
return;
var groupId = groupIdObj?.ToString();
if (string.IsNullOrEmpty(groupId))
return;
component.Groups.RemoveAll(g => g.GroupId == groupId);
component.MessageHistory.Remove(groupId);
component.MutedGroupChats.Remove(groupId);
component.ServerUnreadCounts.Remove(groupId);
component.ActiveInvites.RemoveAll(inv => inv.GroupId == groupId);
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
RequestGroups(uid, component, loaderUid, deviceNetwork);
UpdateUiState(uid, loaderUid, component);
}
private void HandleMessageDeleted(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
{
if (!packet.Data.TryGetValue("message_id", out object? messageIdObj) ||
!packet.Data.TryGetValue("chat_id", out object? chatIdObj))
return;
if (!long.TryParse(messageIdObj?.ToString(), out var messageId))
return;
var chatId = chatIdObj?.ToString();
if (string.IsNullOrEmpty(chatId))
return;
if (component.MessageHistory.TryGetValue(chatId, out var history))
{
history.RemoveAll(m => m.MessageId == messageId);
if (history.Count == 0)
component.MessageHistory.Remove(chatId);
}
UpdateUiState(uid, loaderUid, component);
}
}

View file

@ -55,7 +55,8 @@ public sealed partial class MessengerCartridgeSystem
component.MessageHistory,
component.MutedPersonalChats,
component.MutedGroupChats,
unreadCounts
unreadCounts,
component.ActiveInvites
);
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);

View file

@ -23,4 +23,25 @@ public static class MessengerCommands
public const string CmdMessageReceived = "messenger_message_received";
public const string CmdGroupCreated = "messenger_group_created";
public const string CmdUserAddedToGroup = "messenger_user_added_to_group";
public const string CmdInviteReceived = "messenger_invite_received";
public const string CmdInviteAccepted = "messenger_invite_accepted";
public const string CmdInviteDeclined = "messenger_invite_declined";
public const string CmdInvitesList = "messenger_invites_list";
// Входящие команды для инвайтов
public const string CmdAcceptInvite = "messenger_accept_invite";
public const string CmdDeclineInvite = "messenger_decline_invite";
// Входящие команды для выхода из группы
public const string CmdLeaveGroup = "messenger_leave_group";
// Исходящие команды для выхода из группы
public const string CmdUserLeftGroup = "messenger_user_left_group";
public const string CmdGroupDeleted = "messenger_group_deleted";
// Входящие команды для удаления сообщений
public const string CmdDeleteMessage = "messenger_delete_message";
// Исходящие команды для удаления сообщений
public const string CmdMessageDeleted = "messenger_message_deleted";
}

View file

@ -34,7 +34,7 @@ public sealed partial class MessengerServerComponent : Component
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public int MaxMessageHistory = 5000;
public int MaxMessageHistory = 2000;
/// <summary>
/// Счетчик для генерации уникальных ID групп
@ -66,4 +66,10 @@ public sealed partial class MessengerServerComponent : Component
/// </summary>
[DataField]
public ProtoId<DeviceFrequencyPrototype> PdaFrequencyId = "PDA";
/// <summary>
/// Активные приглашения в группы. Ключ - ID получателя, значение - список приглашений
/// </summary>
[ViewVariables]
public readonly Dictionary<string, List<MessengerGroupInvite>> ActiveInvites = new();
}

View file

@ -78,25 +78,94 @@ public sealed partial class MessengerServerSystem
if (!component.Groups.TryGetValue(groupId, out var group))
return;
if (group.Type != MessengerGroupType.UserCreated)
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
if (group.AutoGroupPrototypeId != null)
pdaFrequency = pdaFreq.Frequency;
}
if (group.Type == MessengerGroupType.UserCreated)
{
if (!component.Users.TryGetValue(args.SenderAddress, out var inviter))
return;
if (group.OwnerId != inviter.UserId)
return;
if (!component.Users.TryGetValue(userId, out var invitee))
return;
if (group.Members.Contains(userId))
return;
// Проверяем, нет ли уже активного инвайта
if (component.ActiveInvites.TryGetValue(userId, out var existingInvites))
{
if (_prototypeManager.TryIndex<MessengerAutoGroupPrototype>(group.AutoGroupPrototypeId, out var autoGroupProto))
{
if (!autoGroupProto.AllowManualMemberManagement)
return;
}
else
{
if (existingInvites.Any(inv => inv.GroupId == groupId))
return;
}
// Создаем инвайт
var invite = new MessengerGroupInvite(
groupId,
group.Name,
inviter.UserId,
inviter.Name,
userId,
GetStationTime()
);
if (!component.ActiveInvites.TryGetValue(userId, out var invites))
{
invites = new List<MessengerGroupInvite>();
component.ActiveInvites[userId] = invites;
}
invites.Add(invite);
var invitePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdInviteReceived,
["group_id"] = groupId,
["group_name"] = group.Name,
["inviter_id"] = inviter.UserId,
["inviter_name"] = inviter.Name,
["created_at"] = invite.CreatedAt.TotalSeconds
};
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, userId, invitePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, userId, invitePayload);
}
// Отправляем обновленный список инвайтов
SendInvitesList(uid, component, userId, serverDevice, pdaFrequency);
return;
}
// Для автоматических групп сохраняем старую логику (если разрешено)
if (group.AutoGroupPrototypeId != null)
{
if (_prototypeManager.TryIndex<MessengerAutoGroupPrototype>(group.AutoGroupPrototypeId, out var autoGroupProto))
{
if (!autoGroupProto.AllowManualMemberManagement)
return;
}
}
else
{
return;
}
}
else
{
return;
}
if (!component.Users.TryGetValue(args.SenderAddress, out var adder))
return;
@ -128,15 +197,6 @@ public sealed partial class MessengerServerSystem
history.Add(systemMessage);
TrimMessageHistory(history, component.MaxMessageHistory);
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
var messagePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
@ -332,4 +392,411 @@ public sealed partial class MessengerServerSystem
}
}
}
/// <summary>
/// Обрабатывает принятие приглашения в группу
/// </summary>
private void HandleAcceptInvite(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(MessengerCommands.CmdAcceptInvite, out NetworkPayload? acceptData))
return;
if (!acceptData.TryGetValue("group_id", out string? groupId))
return;
if (!component.Users.TryGetValue(args.SenderAddress, out var user))
return;
if (!component.ActiveInvites.TryGetValue(user.UserId, out var invites))
return;
var invite = invites.FirstOrDefault(inv => inv.GroupId == groupId);
if (invite == null)
return;
if (!component.Groups.TryGetValue(groupId, out var group))
return;
if (group.Members.Contains(user.UserId))
{
invites.Remove(invite);
if (invites.Count == 0)
component.ActiveInvites.Remove(user.UserId);
return;
}
group.Members.Add(user.UserId);
invites.Remove(invite);
if (invites.Count == 0)
component.ActiveInvites.Remove(user.UserId);
var timestamp = GetStationTime();
var messageText = Loc.GetString("messenger-system-user-joined", ("userName", user.Name));
var messageId = GetNextMessageId(uid, component);
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, groupId, null, false, messageId);
if (!component.MessageHistory.TryGetValue(groupId, out var history))
{
history = new List<MessengerMessage>();
component.MessageHistory[groupId] = history;
}
history.Add(systemMessage);
TrimMessageHistory(history, component.MaxMessageHistory);
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
var messagePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
["sender_id"] = "system",
["sender_name"] = Loc.GetString("messenger-system-name"),
["content"] = messageText,
["timestamp"] = timestamp.TotalSeconds,
["group_id"] = groupId,
["recipient_id"] = string.Empty,
["is_read"] = false,
["message_id"] = systemMessage.MessageId
};
foreach (var memberId in group.Members)
{
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == groupId;
if (!isMemberChatOpen)
{
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
{
memberUnreads = new Dictionary<string, int>();
component.UnreadCounts[memberId] = memberUnreads;
}
memberUnreads.TryGetValue(groupId, out var currentCount);
memberUnreads[groupId] = currentCount + 1;
}
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
}
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserAddedToGroup,
["group_id"] = groupId,
["user_id"] = user.UserId
};
foreach (var memberId in group.Members)
{
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, payload);
}
}
if (component.MessageHistory.TryGetValue(groupId, out var groupHistory) && groupHistory.Count > 0)
{
var sortedMessages = groupHistory.OrderBy(m => m.Timestamp)
.ThenBy(m => m.MessageId)
.ThenBy(m => m.SenderId)
.ToList();
var messagesData = new List<Dictionary<string, object>>();
foreach (var msg in sortedMessages)
{
messagesData.Add(new Dictionary<string, object>
{
["sender_id"] = msg.SenderId,
["sender_name"] = msg.SenderName,
["content"] = msg.Content,
["timestamp"] = msg.Timestamp.TotalSeconds,
["group_id"] = msg.GroupId ?? string.Empty,
["recipient_id"] = msg.RecipientId ?? string.Empty,
["is_read"] = msg.IsRead,
["message_id"] = msg.MessageId
});
}
var historyPayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessagesList,
["messages"] = messagesData,
["chat_id"] = groupId
};
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, user.UserId, historyPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, user.UserId, historyPayload);
}
}
SendInvitesList(uid, component, user.UserId, serverDevice, pdaFrequency);
}
/// <summary>
/// Обрабатывает отклонение приглашения в группу
/// </summary>
private void HandleDeclineInvite(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(MessengerCommands.CmdDeclineInvite, out NetworkPayload? declineData))
return;
if (!declineData.TryGetValue("group_id", out string? groupId))
return;
if (!component.Users.TryGetValue(args.SenderAddress, out var user))
return;
if (!component.ActiveInvites.TryGetValue(user.UserId, out var invites))
return;
var invite = invites.FirstOrDefault(inv => inv.GroupId == groupId);
if (invite == null)
return;
invites.Remove(invite);
if (invites.Count == 0)
component.ActiveInvites.Remove(user.UserId);
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
SendInvitesList(uid, component, user.UserId, serverDevice, pdaFrequency);
}
/// <summary>
/// Отправляет список активных инвайтов пользователю
/// </summary>
private void SendInvitesList(EntityUid uid, MessengerServerComponent component, string userId, DeviceNetworkComponent serverDevice, uint? pdaFrequency)
{
var invitesList = new List<Dictionary<string, object>>();
if (component.ActiveInvites.TryGetValue(userId, out var invites))
{
foreach (var invite in invites)
{
invitesList.Add(new Dictionary<string, object>
{
["group_id"] = invite.GroupId,
["group_name"] = invite.GroupName,
["inviter_id"] = invite.InviterId,
["inviter_name"] = invite.InviterName,
["created_at"] = invite.CreatedAt.TotalSeconds
});
}
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdInvitesList,
["invites"] = invitesList
};
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, userId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, userId, payload);
}
}
/// <summary>
/// Обрабатывает выход пользователя из группы
/// </summary>
private void HandleLeaveGroup(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(MessengerCommands.CmdLeaveGroup, out NetworkPayload? leaveData))
return;
if (!leaveData.TryGetValue("group_id", out string? groupId))
return;
if (!component.Users.TryGetValue(args.SenderAddress, out var user))
return;
if (!component.Groups.TryGetValue(groupId, out var group))
return;
if (!group.Members.Contains(user.UserId))
return;
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
if (group.OwnerId == user.UserId && group.Type == MessengerGroupType.UserCreated)
{
var membersToNotify = new List<string>(group.Members);
component.Groups.Remove(groupId);
component.MessageHistory.Remove(groupId);
foreach (var memberId in membersToNotify)
{
if (component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
{
memberUnreads.Remove(groupId);
if (memberUnreads.Count == 0)
component.UnreadCounts.Remove(memberId);
}
if (component.OpenChats.TryGetValue(memberId, out var openChatId) && openChatId == groupId)
{
component.OpenChats[memberId] = null;
}
var deletePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGroupDeleted,
["group_id"] = groupId
};
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, deletePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, deletePayload);
}
}
}
else
{
group.Members.Remove(user.UserId);
if (group.Type == MessengerGroupType.UserCreated)
{
var timestamp = GetStationTime();
var messageText = Loc.GetString("messenger-system-user-left", ("userName", user.Name));
var messageId = GetNextMessageId(uid, component);
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, groupId, null, false, messageId);
if (!component.MessageHistory.TryGetValue(groupId, out var history))
{
history = new List<MessengerMessage>();
component.MessageHistory[groupId] = history;
}
history.Add(systemMessage);
TrimMessageHistory(history, component.MaxMessageHistory);
var messagePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
["sender_id"] = "system",
["sender_name"] = Loc.GetString("messenger-system-name"),
["content"] = messageText,
["timestamp"] = timestamp.TotalSeconds,
["group_id"] = groupId,
["recipient_id"] = string.Empty,
["is_read"] = false,
["message_id"] = systemMessage.MessageId
};
foreach (var memberId in group.Members)
{
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == groupId;
if (!isMemberChatOpen)
{
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
{
memberUnreads = new Dictionary<string, int>();
component.UnreadCounts[memberId] = memberUnreads;
}
memberUnreads.TryGetValue(groupId, out var currentCount);
memberUnreads[groupId] = currentCount + 1;
}
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
}
}
}
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserLeftGroup,
["group_id"] = groupId,
["user_id"] = user.UserId
};
foreach (var memberId in group.Members)
{
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, payload);
}
}
var leftPayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserLeftGroup,
["group_id"] = groupId,
["user_id"] = user.UserId
};
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, user.UserId, leftPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, user.UserId, leftPayload);
}
if (component.UnreadCounts.TryGetValue(user.UserId, out var userUnreads))
{
userUnreads.Remove(groupId);
if (userUnreads.Count == 0)
component.UnreadCounts.Remove(user.UserId);
}
if (component.OpenChats.TryGetValue(user.UserId, out var userOpenChatId) && userOpenChatId == groupId)
{
component.OpenChats[user.UserId] = null;
}
}
}
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
@ -199,4 +200,85 @@ public sealed partial class MessengerServerSystem
}
}
}
/// <summary>
/// Обрабатывает удаление сообщения пользователем
/// </summary>
private void HandleDeleteMessage(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(MessengerCommands.CmdDeleteMessage, out NetworkPayload? deleteData))
return;
if (!deleteData.TryGetValue("message_id", out object? messageIdObj) ||
!deleteData.TryGetValue("chat_id", out string? chatId))
return;
if (!long.TryParse(messageIdObj?.ToString(), out var messageId))
return;
if (!component.Users.TryGetValue(args.SenderAddress, out var user))
return;
if (!component.MessageHistory.TryGetValue(chatId, out var history))
return;
var message = history.FirstOrDefault(m => m.MessageId == messageId);
if (message == null)
return;
if (message.SenderId != user.UserId)
return;
history.Remove(message);
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
var recipients = new HashSet<string>();
if (chatId.StartsWith("personal_"))
{
var parts = chatId.Split('_');
if (parts.Length >= 3)
{
recipients.Add(parts[1]);
recipients.Add(parts[2]);
}
}
else
{
if (component.Groups.TryGetValue(chatId, out var group))
{
foreach (var memberId in group.Members)
{
recipients.Add(memberId);
}
}
}
var deletePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageDeleted,
["message_id"] = messageId,
["chat_id"] = chatId
};
foreach (var recipientId in recipients)
{
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, recipientId, deletePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, recipientId, deletePayload);
}
}
}
}

View file

@ -449,19 +449,6 @@ public sealed partial class MessengerServerSystem
{
group.Members.Add(userId);
var timestamp = GetStationTime();
var messageText = Loc.GetString("messenger-system-user-added", ("userName", userName));
var messageId = GetNextMessageId(uid, component);
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, autoGroupProto.GroupId, null, false, messageId);
if (!component.MessageHistory.TryGetValue(autoGroupProto.GroupId, out var history))
{
history = new List<MessengerMessage>();
component.MessageHistory[autoGroupProto.GroupId] = history;
}
history.Add(systemMessage);
TrimMessageHistory(history, component.MaxMessageHistory);
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
continue;
@ -471,41 +458,67 @@ public sealed partial class MessengerServerSystem
pdaFrequency = pdaFreq.Frequency;
}
var messagePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
["sender_id"] = "system",
["sender_name"] = Loc.GetString("messenger-system-name"),
["content"] = messageText,
["timestamp"] = timestamp.TotalSeconds,
["group_id"] = autoGroupProto.GroupId,
["recipient_id"] = string.Empty,
["is_read"] = false,
["message_id"] = systemMessage.MessageId
};
var isDepartmentChat = group.Type == MessengerGroupType.Automatic && group.AutoGroupPrototypeId != null;
foreach (var memberId in group.Members)
if (!isDepartmentChat)
{
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == autoGroupProto.GroupId;
var timestamp = GetStationTime();
var messageText = Loc.GetString("messenger-system-user-added", ("userName", userName));
var messageId = GetNextMessageId(uid, component);
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, autoGroupProto.GroupId, null, false, messageId);
if (!isMemberChatOpen)
if (!component.MessageHistory.TryGetValue(autoGroupProto.GroupId, out var history))
{
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
history = new List<MessengerMessage>();
component.MessageHistory[autoGroupProto.GroupId] = history;
}
history.Add(systemMessage);
TrimMessageHistory(history, component.MaxMessageHistory);
}
if (!isDepartmentChat)
{
var timestamp = GetStationTime();
var messageText = Loc.GetString("messenger-system-user-added", ("userName", userName));
var messageId = GetNextMessageId(uid, component);
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, autoGroupProto.GroupId, null, false, messageId);
var messagePayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
["sender_id"] = "system",
["sender_name"] = Loc.GetString("messenger-system-name"),
["content"] = messageText,
["timestamp"] = timestamp.TotalSeconds,
["group_id"] = autoGroupProto.GroupId,
["recipient_id"] = string.Empty,
["is_read"] = false,
["message_id"] = systemMessage.MessageId
};
foreach (var memberId in group.Members)
{
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == autoGroupProto.GroupId;
if (!isMemberChatOpen)
{
memberUnreads = new Dictionary<string, int>();
component.UnreadCounts[memberId] = memberUnreads;
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
{
memberUnreads = new Dictionary<string, int>();
component.UnreadCounts[memberId] = memberUnreads;
}
memberUnreads.TryGetValue(autoGroupProto.GroupId, out var currentCount);
memberUnreads[autoGroupProto.GroupId] = currentCount + 1;
}
memberUnreads.TryGetValue(autoGroupProto.GroupId, out var currentCount);
memberUnreads[autoGroupProto.GroupId] = currentCount + 1;
}
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
}
}
}

View file

@ -118,6 +118,18 @@ public sealed partial class MessengerServerSystem : EntitySystem
case MessengerCommands.CmdGetMessages:
HandleGetMessages(uid, component, args);
break;
case MessengerCommands.CmdAcceptInvite:
HandleAcceptInvite(uid, component, args);
break;
case MessengerCommands.CmdDeclineInvite:
HandleDeclineInvite(uid, component, args);
break;
case MessengerCommands.CmdLeaveGroup:
HandleLeaveGroup(uid, component, args);
break;
case MessengerCommands.CmdDeleteMessage:
HandleDeleteMessage(uid, component, args);
break;
default:
Sawmill.Warning($"Unknown command received: {command} from {args.SenderAddress}");
break;

View file

@ -17,6 +17,7 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
public readonly string? UserId;
public readonly string? ChatId;
public readonly bool? IsMuted;
public readonly long? MessageId;
public MessengerUiMessageEvent(
MessengerUiAction action,
@ -26,7 +27,8 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
string? groupName = null,
string? userId = null,
string? chatId = null,
bool? isMuted = null)
bool? isMuted = null,
long? messageId = null)
{
Action = action;
RecipientId = recipientId;
@ -36,6 +38,7 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
UserId = userId;
ChatId = chatId;
IsMuted = isMuted;
MessageId = messageId;
}
}
@ -52,5 +55,9 @@ public enum MessengerUiAction
RequestUsers,
RequestGroups,
RequestMessages,
ToggleMute
ToggleMute,
AcceptInvite,
DeclineInvite,
LeaveGroup,
DeleteMessage
}

View file

@ -54,6 +54,11 @@ public sealed class MessengerUiState : BoundUserInterfaceState
/// </summary>
public Dictionary<string, int> UnreadCounts { get; }
/// <summary>
/// Активные приглашения в группы
/// </summary>
public List<MessengerGroupInvite> ActiveInvites { get; }
public MessengerUiState(
bool isRegistered,
bool serverAvailable,
@ -63,7 +68,8 @@ public sealed class MessengerUiState : BoundUserInterfaceState
Dictionary<string, List<MessengerMessage>> messageHistory,
HashSet<string> mutedPersonalChats,
HashSet<string> mutedGroupChats,
Dictionary<string, int> unreadCounts)
Dictionary<string, int> unreadCounts,
List<MessengerGroupInvite> activeInvites)
{
IsRegistered = isRegistered;
ServerAvailable = serverAvailable;
@ -74,5 +80,6 @@ public sealed class MessengerUiState : BoundUserInterfaceState
MutedPersonalChats = mutedPersonalChats;
MutedGroupChats = mutedGroupChats;
UnreadCounts = unreadCounts;
ActiveInvites = activeInvites;
}
}

View file

@ -0,0 +1,50 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Messenger;
/// <summary>
/// Приглашение в группу мессенджера
/// </summary>
[Serializable, NetSerializable]
public sealed class MessengerGroupInvite
{
/// <summary>
/// ID группы, в которую приглашают
/// </summary>
public string GroupId { get; set; }
/// <summary>
/// Название группы
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// ID пользователя, который отправил приглашение
/// </summary>
public string InviterId { get; set; }
/// <summary>
/// Имя пользователя, который отправил приглашение
/// </summary>
public string InviterName { get; set; }
/// <summary>
/// ID пользователя, которому отправлено приглашение
/// </summary>
public string InviteeId { get; set; }
/// <summary>
/// Время создания приглашения
/// </summary>
public TimeSpan CreatedAt { get; set; }
public MessengerGroupInvite(string groupId, string groupName, string inviterId, string inviterName, string inviteeId, TimeSpan createdAt)
{
GroupId = groupId;
GroupName = groupName;
InviterId = inviterId;
InviterName = inviterName;
InviteeId = inviteeId;
CreatedAt = createdAt;
}
}

View file

@ -4,6 +4,8 @@ messenger-status-connected = Connected
messenger-search-placeholder = Search...
messenger-tab-personal = Personal
messenger-tab-groups = Groups
messenger-tab-invites = Invites
messenger-no-invites = No invites
messenger-create-group-button = Create group
messenger-chat-select = Select chat
messenger-mute-tooltip = Mute
@ -46,3 +48,13 @@ messenger-emoji-favorite-hint = ПКМ по общему списку для д
messenger-emoji-all-title = All emojis
messenger-user-unknown = Unknown
messenger-system-name = System
messenger-leave-group = Leave group
messenger-invite-member-button = Invite
messenger-invites-header = Invites
messenger-invite-info = { $inviter } invites you
messenger-invite-accept = Accept
messenger-invite-decline = Decline
messenger-invite-user-to-group-title = Invite to { $groupName }
messenger-delete-message = Delete message
messenger-system-user-joined = { $userName } joined the group
messenger-system-user-left = { $userName } left the group

View file

@ -4,6 +4,8 @@ messenger-status-connected = Подключено
messenger-search-placeholder = Поиск...
messenger-tab-personal = Личные
messenger-tab-groups = Группы
messenger-tab-invites = Приглашения
messenger-no-invites = Нет приглашений
messenger-create-group-button = Создать группу
messenger-chat-select = Выберите чат
messenger-mute-tooltip = Отключить звук
@ -47,3 +49,13 @@ messenger-emoji-favorite-hint = ПКМ по общему списку для д
messenger-emoji-all-title = Все смайлики
messenger-user-unknown = Неизвестно
messenger-system-name = Система
messenger-leave-group = Выйти из группы
messenger-invite-member-button = Пригласить
messenger-invites-header = Приглашения
messenger-invite-info = { $inviter } приглашает вас
messenger-invite-accept = Принять
messenger-invite-decline = Отклонить
messenger-invite-user-to-group-title = Пригласить в { $groupName }
messenger-delete-message = Удалить сообщение
messenger-system-user-joined = { $userName } присоединился(ась) к группе
messenger-system-user-left = { $userName } покинул(а) группу