Добавлена поддержка закрепления чатов и отображения иконок ролей пользователей в мессенджере.

This commit is contained in:
Vigers Ray 2026-01-28 03:09:53 +01:00
parent 19274a2d49
commit 5a72fbd4a0
16 changed files with 423 additions and 38 deletions

View file

@ -12,6 +12,13 @@
Orientation="Horizontal"
Margin="0,0,0,4"
Name="Header">
<TextureRect
Name="JobIconRect"
SetWidth="20"
SetHeight="20"
Stretch="Scale"
Margin="0,0,4,0"
Visible="False" />
<Label
Name="SenderLabel"
Text=""

View file

@ -2,6 +2,7 @@ using Content.Client._Sunrise.Messenger;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared._Sunrise.Messenger;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@ -9,6 +10,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
@ -18,6 +20,7 @@ public sealed partial class MessagePanel : PanelContainer
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private ClientEmojiSystem? EmojiSystem => _entitySystemManager.GetEntitySystemOrNull<ClientEmojiSystem>();
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
@ -53,6 +56,25 @@ public sealed partial class MessagePanel : PanelContainer
SenderLabel.Text = message.SenderName;
TimeLabel.Text = message.Timestamp.ToString(@"hh\:mm");
if (message.SenderJobIconId != null && _prototypeManager.TryIndex(message.SenderJobIconId.Value, out JobIconPrototype? jobIcon))
{
JobIconRect.Texture = GetSpriteSystem().Frame0(jobIcon.Icon);
JobIconRect.Visible = true;
}
else
{
var unknownIconId = new ProtoId<JobIconPrototype>("JobIconUnknown");
if (_prototypeManager.TryIndex(unknownIconId, out JobIconPrototype? unknownIcon))
{
JobIconRect.Texture = GetSpriteSystem().Frame0(unknownIcon.Icon);
JobIconRect.Visible = true;
}
else
{
JobIconRect.Visible = false;
}
}
var parsedContent = EmojiSystem?.ParseEmojis(message.Content) ?? message.Content;
ContentLabel.SetMessage(FormattedMessage.FromMarkupPermissive(parsedContent), MessageTagsAllowed);

View file

@ -37,6 +37,8 @@ public sealed partial class MessengerUi : UIFragment
SendMessengerMessage(MessengerUiAction.LeaveGroup, userInterface, groupId: groupId);
_fragment.OnDeleteMessage += (chatId, messageId) =>
SendMessengerMessage(MessengerUiAction.DeleteMessage, userInterface, chatId: chatId, messageId: messageId);
_fragment.OnTogglePin += (chatId) =>
SendMessengerMessage(MessengerUiAction.TogglePin, userInterface, chatId: chatId);
}
public override void UpdateState(BoundUserInterfaceState state)

View file

@ -16,7 +16,9 @@ using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Content.Shared.StatusIcon;
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
@ -33,6 +35,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
public event Action<string>? OnDeclineInvite;
public event Action<string>? OnLeaveGroup;
public event Action<string, long>? OnDeleteMessage;
public event Action<string>? OnTogglePin;
private string? _currentChatId;
private MessengerUiState? _currentState;
@ -52,11 +55,13 @@ public sealed partial class MessengerUiFragment : BoxContainer
private Dictionary<string, int>? _lastUnreadCounts;
private HashSet<string>? _lastUserIds;
private HashSet<string>? _lastGroupIds;
private HashSet<string>? _lastPinnedChats;
private int _lastActiveTab = -1;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private CreateGroupDialog? _createGroupDialog;
private AddUserDialog? _addUserDialog;
@ -413,9 +418,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
}
}
if (textContainer == null || nameLabel == null)
return;
var existingCounter = textContainer.Children.OfType<Label>()
.FirstOrDefault(l => l != nameLabel && l.StyleClasses.Contains("Bold"));
@ -492,15 +494,18 @@ public sealed partial class MessengerUiFragment : BoxContainer
{
var currentUserIds = new HashSet<string>(state.Users.Select(u => u.UserId));
var currentGroupIds = new HashSet<string>(state.Groups.Select(g => g.GroupId));
var currentPinnedChats = new HashSet<string>(state.PinnedChats);
var tabChanged = _lastActiveTab != _activeTab;
var needsUpdate = _lastUserIds == null ||
_lastGroupIds == null ||
_lastUnreadCounts == null ||
_lastPinnedChats == null ||
tabChanged ||
!currentUserIds.SetEquals(_lastUserIds) ||
!currentGroupIds.SetEquals(_lastGroupIds) ||
!currentPinnedChats.SetEquals(_lastPinnedChats) ||
!AreUnreadCountsEqual(_lastUnreadCounts, state.UnreadCounts) ||
_searchFilter != string.Empty;
@ -513,6 +518,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
_lastUserIds = currentUserIds;
_lastGroupIds = currentGroupIds;
_lastUnreadCounts = new Dictionary<string, int>(state.UnreadCounts);
_lastPinnedChats = new HashSet<string>(currentPinnedChats);
_lastActiveTab = _activeTab;
ChatsContainer.RemoveAllChildren();
@ -536,13 +542,18 @@ public sealed partial class MessengerUiFragment : BoxContainer
if (_activeTab == 0)
{
foreach (var user in state.Users)
{
if (user.UserId == state.CurrentUserId)
continue;
var usersList = state.Users.Where(u => u.UserId != state.CurrentUserId).ToList();
var pinnedChats = state.PinnedChats;
var sortedUsers = usersList.OrderByDescending(u => pinnedChats.Contains(GetPersonalChatId(u.UserId)))
.ThenBy(u => u.Name)
.ToList();
foreach (var user in sortedUsers)
{
var userName = user.Name;
var chatId = GetPersonalChatId(user.UserId);
var isPinned = pinnedChats.Contains(chatId);
if (!string.IsNullOrEmpty(searchLower) && !userName.ToLowerInvariant().Contains(searchLower))
continue;
@ -571,9 +582,52 @@ public sealed partial class MessengerUiFragment : BoxContainer
HorizontalExpand = true
};
if (isPinned)
{
var starLabel = new Label
{
Text = "★",
HorizontalExpand = false,
Margin = new Thickness(0, 0, 4, 0),
VerticalAlignment = VAlignment.Center
};
textContainer.AddChild(starLabel);
}
if (user.JobIconId != null && _prototypeManager.TryIndex(user.JobIconId.Value, out JobIconPrototype? jobIcon))
{
var iconRect = new TextureRect
{
Texture = GetSpriteSystem().Frame0(jobIcon.Icon),
SetWidth = 20,
SetHeight = 20,
Stretch = TextureRect.StretchMode.Scale,
Margin = new Thickness(0, 0, 4, 0)
};
textContainer.AddChild(iconRect);
}
else
{
var unknownIconId = new ProtoId<JobIconPrototype>("JobIconUnknown");
if (_prototypeManager.TryIndex(unknownIconId, out JobIconPrototype? unknownIcon))
{
var iconRect = new TextureRect
{
Texture = GetSpriteSystem().Frame0(unknownIcon.Icon),
SetWidth = 20,
SetHeight = 20,
Stretch = TextureRect.StretchMode.Scale,
Margin = new Thickness(0, 0, 4, 0)
};
textContainer.AddChild(iconRect);
}
}
var displayName = userName;
var nameLabel = new Label
{
Text = userName,
Text = displayName,
HorizontalExpand = true,
ClipText = true,
Align = Label.AlignMode.Left
@ -598,6 +652,29 @@ public sealed partial class MessengerUiFragment : BoxContainer
button.AddChild(textContainer);
button.OnPressed += _ => SelectChat(chatId, userName);
button.OnKeyBindDown += args =>
{
if (args.Function == EngineKeyFunctions.UIRightClick)
{
args.Handle();
if (_currentState != null)
{
if (_currentState.PinnedChats.Contains(chatId))
_currentState.PinnedChats.Remove(chatId);
else
_currentState.PinnedChats.Add(chatId);
_lastPinnedChats = null;
UpdateChatsList(_currentState);
}
OnTogglePin?.Invoke(chatId);
}
};
buttonContainer.AddChild(button);
ChatsContainer.AddChild(buttonContainer);
@ -605,7 +682,13 @@ public sealed partial class MessengerUiFragment : BoxContainer
}
else if (_activeTab == 1)
{
foreach (var group in state.Groups)
var pinnedChats = state.PinnedChats;
var sortedGroups = state.Groups.OrderByDescending(g => pinnedChats.Contains(g.GroupId))
.ThenBy(g => g.Name)
.ToList();
foreach (var group in sortedGroups)
{
var groupContainer = new BoxContainer
{
@ -616,6 +699,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
var groupId = group.GroupId;
var groupName = group.Name;
var isPinned = pinnedChats.Contains(groupId);
if (!string.IsNullOrEmpty(searchLower) && !groupName.ToLowerInvariant().Contains(searchLower))
continue;
@ -637,9 +721,23 @@ public sealed partial class MessengerUiFragment : BoxContainer
HorizontalExpand = true
};
if (isPinned)
{
var starLabel = new Label
{
Text = "★",
HorizontalExpand = false,
Margin = new Thickness(0, 0, 4, 0),
VerticalAlignment = VAlignment.Center
};
textContainer.AddChild(starLabel);
}
var displayName = groupName;
var nameLabel = new Label
{
Text = groupName,
Text = displayName,
HorizontalExpand = true,
ClipText = true,
Align = Label.AlignMode.Left
@ -664,6 +762,29 @@ public sealed partial class MessengerUiFragment : BoxContainer
button.AddChild(textContainer);
button.OnPressed += _ => SelectChat(groupId, groupName);
button.OnKeyBindDown += args =>
{
if (args.Function == EngineKeyFunctions.UIRightClick)
{
args.Handle();
if (_currentState != null)
{
if (_currentState.PinnedChats.Contains(groupId))
_currentState.PinnedChats.Remove(groupId);
else
_currentState.PinnedChats.Add(groupId);
_lastPinnedChats = null;
UpdateChatsList(_currentState);
}
OnTogglePin?.Invoke(groupId);
}
};
groupContainer.AddChild(button);
ChatsContainer.AddChild(groupContainer);
@ -992,9 +1113,49 @@ public sealed partial class MessengerUiFragment : BoxContainer
HorizontalExpand = true
};
var displayText = isMemberOwner
? $"★ {userName}"
: userName;
if (isMemberOwner)
{
var ownerStarLabel = new Label
{
Text = "★",
HorizontalExpand = false,
Margin = new Thickness(0, 0, 4, 0),
VerticalAlignment = VAlignment.Center
};
memberContainer.AddChild(ownerStarLabel);
}
if (user != null)
{
if (user.JobIconId != null && _prototypeManager.TryIndex(user.JobIconId.Value, out JobIconPrototype? jobIcon))
{
var iconRect = new TextureRect
{
Texture = GetSpriteSystem().Frame0(jobIcon.Icon),
SetWidth = 20,
SetHeight = 20,
Stretch = TextureRect.StretchMode.Scale,
Margin = new Thickness(0, 0, 4, 0)
};
memberContainer.AddChild(iconRect);
}
else
{
var unknownIconId = new ProtoId<JobIconPrototype>("JobIconUnknown");
if (_prototypeManager.TryIndex(unknownIconId, out JobIconPrototype? unknownIcon))
{
var iconRect = new TextureRect
{
Texture = GetSpriteSystem().Frame0(unknownIcon.Icon),
SetWidth = 20,
SetHeight = 20,
Stretch = TextureRect.StretchMode.Scale,
Margin = new Thickness(0, 0, 4, 0)
};
memberContainer.AddChild(iconRect);
}
}
}
var tooltipText = isMemberOwner
? Loc.GetString("messenger-member-owner", ("name", userName))
@ -1002,7 +1163,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
var memberLabel = new Label
{
Text = displayText,
Text = userName,
HorizontalExpand = true,
ClipText = true,
ToolTip = tooltipText

View file

@ -103,4 +103,10 @@ public sealed partial class MessengerCartridgeComponent : Component
/// </summary>
[ViewVariables]
public List<MessengerGroupInvite> ActiveInvites = new();
/// <summary>
/// Закрепленные чаты (chatId)
/// </summary>
[ViewVariables]
public HashSet<string> PinnedChats = new();
}

View file

@ -68,6 +68,10 @@ public sealed partial class MessengerCartridgeSystem
if (message.ChatId != null && message.MessageId.HasValue)
DeleteMessage(uid, component, loaderUid, deviceNetwork, message.ChatId, message.MessageId.Value);
break;
case MessengerUiAction.TogglePin:
if (message.ChatId != null)
TogglePin(uid, component, message.ChatId);
break;
}
}

View file

@ -6,6 +6,8 @@ using Content.Shared.DeviceNetwork.Events;
using Content.Shared._Sunrise.Messenger;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.PDA.Ringer;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
@ -116,11 +118,17 @@ public sealed partial class MessengerCartridgeSystem
userData.TryGetValue("job_title", out object? jobTitleObj);
userData.TryGetValue("department_id", out object? departmentIdObj);
userData.TryGetValue("job_icon_id", out object? jobIconIdObj);
var jobTitle = jobTitleObj?.ToString();
var departmentId = departmentIdObj?.ToString();
ProtoId<JobIconPrototype>? jobIconId = null;
if (jobIconIdObj != null && !string.IsNullOrEmpty(jobIconIdObj.ToString()))
{
jobIconId = new ProtoId<JobIconPrototype>(jobIconIdObj.ToString()!);
}
users.Add(new MessengerUser(userId, userName, jobTitle, departmentId));
users.Add(new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId));
}
component.Users = users;
@ -273,6 +281,7 @@ public sealed partial class MessengerCartridgeSystem
messageData.TryGetValue("recipient_id", out object? recipientIdObj);
messageData.TryGetValue("is_read", out object? isReadObj);
messageData.TryGetValue("message_id", out object? messageIdObj);
messageData.TryGetValue("sender_job_icon_id", out object? senderJobIconIdObj);
var groupId = groupIdObj?.ToString();
var recipientId = recipientIdObj?.ToString();
@ -289,8 +298,14 @@ public sealed partial class MessengerCartridgeSystem
messageId = messageIdValue;
}
ProtoId<JobIconPrototype>? senderJobIconId = null;
if (senderJobIconIdObj != null && !string.IsNullOrEmpty(senderJobIconIdObj.ToString()))
{
senderJobIconId = new ProtoId<JobIconPrototype>(senderJobIconIdObj.ToString()!);
}
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
messages.Add(new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId));
messages.Add(new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId));
}
if (!string.IsNullOrEmpty(chatId) && messages.Count >= 0)
@ -403,6 +418,7 @@ public sealed partial class MessengerCartridgeSystem
packet.Data.TryGetValue("recipient_id", out object? recipientIdObj);
packet.Data.TryGetValue("is_read", out object? isReadObj);
packet.Data.TryGetValue("message_id", out object? messageIdObj);
packet.Data.TryGetValue("sender_job_icon_id", out object? senderJobIconIdObj);
var groupId = groupIdObj?.ToString();
var recipientId = recipientIdObj?.ToString();
@ -419,8 +435,14 @@ public sealed partial class MessengerCartridgeSystem
messageId = messageIdValue;
}
ProtoId<JobIconPrototype>? senderJobIconId = null;
if (senderJobIconIdObj != null && !string.IsNullOrEmpty(senderJobIconIdObj.ToString()))
{
senderJobIconId = new ProtoId<JobIconPrototype>(senderJobIconIdObj.ToString()!);
}
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
var message = new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId);
var message = new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId);
string chatId;
if (!string.IsNullOrEmpty(groupId))

View file

@ -56,7 +56,8 @@ public sealed partial class MessengerCartridgeSystem
component.MutedPersonalChats,
component.MutedGroupChats,
unreadCounts,
component.ActiveInvites
component.ActiveInvites,
component.PinnedChats
);
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
@ -84,4 +85,15 @@ public sealed partial class MessengerCartridgeSystem
if (component.LoaderUid.HasValue)
UpdateUiState(uid, component.LoaderUid.Value, component);
}
private void TogglePin(EntityUid uid, MessengerCartridgeComponent component, string chatId)
{
if (component.PinnedChats.Contains(chatId))
component.PinnedChats.Remove(chatId);
else
component.PinnedChats.Add(chatId);
if (component.LoaderUid.HasValue)
UpdateUiState(uid, component.LoaderUid.Value, component);
}
}

View file

@ -22,6 +22,8 @@ public sealed partial class MessengerServerSystem
if (!component.Users.TryGetValue(args.SenderAddress, out var sender))
return;
UpdateUserInfoFromPda(uid, component, args.SenderAddress, sender);
var timestamp = GetStationTime();
if (messageData.TryGetValue("group_id", out string? groupId) && !string.IsNullOrWhiteSpace(groupId))
@ -40,7 +42,7 @@ public sealed partial class MessengerServerSystem
return;
var messageId = GetNextMessageId(uid, component);
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false, messageId);
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false, messageId, sender.JobIconId);
var chatId = GetPersonalChatId(sender.UserId, recipientId);
if (!component.MessageHistory.TryGetValue(chatId, out var history))
@ -88,7 +90,8 @@ public sealed partial class MessengerServerSystem
["group_id"] = message.GroupId ?? string.Empty,
["recipient_id"] = message.RecipientId ?? string.Empty,
["is_read"] = message.IsRead,
["message_id"] = message.MessageId
["message_id"] = message.MessageId,
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
};
if (pdaFrequency.HasValue)
@ -136,7 +139,7 @@ public sealed partial class MessengerServerSystem
return;
var messageId = GetNextMessageId(uid, component);
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId, null, false, messageId);
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId, null, false, messageId, sender.JobIconId);
if (!component.MessageHistory.TryGetValue(groupId, out var history))
{
@ -185,7 +188,8 @@ public sealed partial class MessengerServerSystem
["group_id"] = message.GroupId ?? string.Empty,
["recipient_id"] = message.RecipientId ?? string.Empty,
["is_read"] = message.IsRead,
["message_id"] = message.MessageId
["message_id"] = message.MessageId,
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
};
foreach (var memberId in group.Members)

View file

@ -26,7 +26,8 @@ public sealed partial class MessengerServerSystem
["user_id"] = user.UserId,
["user_name"] = user.Name,
["job_title"] = user.JobTitle ?? string.Empty,
["department_id"] = user.DepartmentId ?? string.Empty
["department_id"] = user.DepartmentId ?? string.Empty,
["job_icon_id"] = user.JobIconId?.Id ?? string.Empty
});
}
@ -208,7 +209,8 @@ public sealed partial class MessengerServerSystem
["group_id"] = msg.GroupId ?? string.Empty,
["recipient_id"] = msg.RecipientId ?? string.Empty,
["is_read"] = msg.IsRead,
["message_id"] = msg.MessageId
["message_id"] = msg.MessageId,
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty
});
}
}
@ -250,7 +252,8 @@ public sealed partial class MessengerServerSystem
["group_id"] = msg.GroupId ?? string.Empty,
["recipient_id"] = msg.RecipientId ?? string.Empty,
["is_read"] = msg.IsRead,
["message_id"] = msg.MessageId
["message_id"] = msg.MessageId,
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty
});
}
@ -298,7 +301,8 @@ public sealed partial class MessengerServerSystem
["group_id"] = message.GroupId ?? string.Empty,
["recipient_id"] = message.RecipientId ?? string.Empty,
["is_read"] = message.IsRead,
["message_id"] = message.MessageId
["message_id"] = message.MessageId,
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
});
}

View file

@ -9,6 +9,8 @@ using Content.Shared.Access.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.GameTicking;
using Content.Shared.CartridgeLoader;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
namespace Content.Server._Sunrise.Messenger;
@ -133,6 +135,7 @@ public sealed partial class MessengerServerSystem
string? jobTitle = null;
string? departmentId = null;
ProtoId<JobIconPrototype>? jobIconId = null;
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
{
@ -141,9 +144,10 @@ public sealed partial class MessengerServerSystem
{
departmentId = idCard.JobDepartments[0];
}
jobIconId = idCard.JobIcon;
}
var user = new MessengerUser(userId, userName, jobTitle, departmentId);
var user = new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId);
component.Users[userId] = user;
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
@ -171,7 +175,8 @@ public sealed partial class MessengerServerSystem
["user_id"] = userId,
["user_name"] = userName,
["job_title"] = jobTitle ?? string.Empty,
["department_id"] = departmentId ?? string.Empty
["department_id"] = departmentId ?? string.Empty,
["job_icon_id"] = jobIconId?.Id ?? string.Empty
};
if (_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, userId))
@ -188,7 +193,8 @@ public sealed partial class MessengerServerSystem
["user_id"] = u.UserId,
["user_name"] = u.Name,
["job_title"] = u.JobTitle ?? string.Empty,
["department_id"] = u.DepartmentId ?? string.Empty
["department_id"] = u.DepartmentId ?? string.Empty,
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
}).ToList()
};
@ -301,6 +307,7 @@ public sealed partial class MessengerServerSystem
string? userName = null;
string? jobTitle = null;
string? departmentId = null;
ProtoId<JobIconPrototype>? jobIconId = null;
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
{
@ -310,6 +317,7 @@ public sealed partial class MessengerServerSystem
{
departmentId = idCard.JobDepartments[0];
}
jobIconId = idCard.JobIcon;
}
if (string.IsNullOrWhiteSpace(userName))
@ -317,7 +325,7 @@ public sealed partial class MessengerServerSystem
userName = pda.OwnerName ?? Loc.GetString("messenger-user-unknown");
}
var user = new MessengerUser(userId, userName, jobTitle, departmentId);
var user = new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId);
component.Users[userId] = user;
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
@ -345,7 +353,8 @@ public sealed partial class MessengerServerSystem
["user_id"] = userId,
["user_name"] = userName,
["job_title"] = jobTitle ?? string.Empty,
["department_id"] = departmentId ?? string.Empty
["department_id"] = departmentId ?? string.Empty,
["job_icon_id"] = jobIconId?.Id ?? string.Empty
};
if (_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, args.SenderAddress))
@ -361,7 +370,8 @@ public sealed partial class MessengerServerSystem
["user_id"] = u.UserId,
["user_name"] = u.Name,
["job_title"] = u.JobTitle ?? string.Empty,
["department_id"] = u.DepartmentId ?? string.Empty
["department_id"] = u.DepartmentId ?? string.Empty,
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
}).ToList()
};
@ -586,4 +596,89 @@ public sealed partial class MessengerServerSystem
}
}
}
/// <summary>
/// Обновляет информацию о пользователе из его PDA по адресу DeviceNetwork
/// </summary>
private void UpdateUserInfoFromPda(EntityUid uid, MessengerServerComponent component, string userId, MessengerUser user)
{
var pdaQuery = EntityQueryEnumerator<PdaComponent, DeviceNetworkComponent>();
EntityUid? foundPda = null;
while (pdaQuery.MoveNext(out var pdaUid, out var pda, out var deviceNetwork))
{
if (deviceNetwork.Address == userId)
{
foundPda = pdaUid;
break;
}
}
if (foundPda == null)
return;
if (!TryComp<PdaComponent>(foundPda.Value, out var pdaComp))
return;
string? jobTitle = null;
string? departmentId = null;
ProtoId<JobIconPrototype>? jobIconId = null;
if (pdaComp.ContainedId != null && TryComp<IdCardComponent>(pdaComp.ContainedId.Value, out var idCard))
{
jobTitle = idCard.LocalizedJobTitle;
if (idCard.JobDepartments.Count > 0)
{
departmentId = idCard.JobDepartments[0];
}
jobIconId = idCard.JobIcon;
}
var needsUpdate = user.JobTitle != jobTitle || user.DepartmentId != departmentId || user.JobIconId != jobIconId;
if (needsUpdate)
{
user.JobTitle = jobTitle;
user.DepartmentId = departmentId;
user.JobIconId = jobIconId;
// Отправляем обновленный список пользователей всем клиентам
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
return;
uint? pdaFrequency = null;
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
{
pdaFrequency = pdaFreq.Frequency;
}
var notifyPayload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUsersList,
["users"] = component.Users.Values.Select(u => new Dictionary<string, object>
{
["user_id"] = u.UserId,
["user_name"] = u.Name,
["job_title"] = u.JobTitle ?? string.Empty,
["department_id"] = u.DepartmentId ?? string.Empty,
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
}).ToList()
};
foreach (var existingUserId in component.Users.Keys)
{
if (_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, existingUserId))
{
if (pdaFrequency.HasValue)
{
_deviceNetwork.QueuePacket(uid, existingUserId, notifyPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
}
else
{
_deviceNetwork.QueuePacket(uid, existingUserId, notifyPayload);
}
}
}
}
}
}

View file

@ -59,5 +59,6 @@ public enum MessengerUiAction
AcceptInvite,
DeclineInvite,
LeaveGroup,
DeleteMessage
DeleteMessage,
TogglePin
}

View file

@ -59,6 +59,11 @@ public sealed class MessengerUiState : BoundUserInterfaceState
/// </summary>
public List<MessengerGroupInvite> ActiveInvites { get; }
/// <summary>
/// Закрепленные чаты (chatId)
/// </summary>
public HashSet<string> PinnedChats { get; }
public MessengerUiState(
bool isRegistered,
bool serverAvailable,
@ -69,7 +74,8 @@ public sealed class MessengerUiState : BoundUserInterfaceState
HashSet<string> mutedPersonalChats,
HashSet<string> mutedGroupChats,
Dictionary<string, int> unreadCounts,
List<MessengerGroupInvite> activeInvites)
List<MessengerGroupInvite> activeInvites,
HashSet<string> pinnedChats)
{
IsRegistered = isRegistered;
ServerAvailable = serverAvailable;
@ -81,5 +87,6 @@ public sealed class MessengerUiState : BoundUserInterfaceState
MutedGroupChats = mutedGroupChats;
UnreadCounts = unreadCounts;
ActiveInvites = activeInvites;
PinnedChats = pinnedChats;
}
}

View file

@ -1,3 +1,5 @@
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Messenger;
@ -48,7 +50,12 @@ public sealed class MessengerMessage
/// </summary>
public long MessageId { get; set; }
public MessengerMessage(string senderId, string senderName, string content, TimeSpan timestamp, string? groupId = null, string? recipientId = null, bool isRead = false, long messageId = 0)
/// <summary>
/// ID иконки роли отправителя (опционально)
/// </summary>
public ProtoId<JobIconPrototype>? SenderJobIconId { get; set; }
public MessengerMessage(string senderId, string senderName, string content, TimeSpan timestamp, string? groupId = null, string? recipientId = null, bool isRead = false, long messageId = 0, ProtoId<JobIconPrototype>? senderJobIconId = null)
{
SenderId = senderId;
SenderName = senderName;
@ -58,5 +65,6 @@ public sealed class MessengerMessage
RecipientId = recipientId;
IsRead = isRead;
MessageId = messageId;
SenderJobIconId = senderJobIconId;
}
}

View file

@ -1,3 +1,5 @@
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Messenger;
@ -28,11 +30,17 @@ public sealed class MessengerUser
/// </summary>
public string? DepartmentId { get; set; }
public MessengerUser(string userId, string name, string? jobTitle = null, string? departmentId = null)
/// <summary>
/// ID иконки роли пользователя (опционально)
/// </summary>
public ProtoId<JobIconPrototype>? JobIconId { get; set; }
public MessengerUser(string userId, string name, string? jobTitle = null, string? departmentId = null, ProtoId<JobIconPrototype>? jobIconId = null)
{
UserId = userId;
Name = name;
JobTitle = jobTitle;
DepartmentId = departmentId;
JobIconId = jobIconId;
}
}

View file

@ -25300,5 +25300,27 @@
\u0438\u044F \u0432 \u043B\u0438\u0447\u043D\u044B\u0445 \u0438 \u0433\u0440\
\u0443\u043F\u043E\u0432\u044B\u0445 \u0447\u0430\u0442\u0430\u0445."
type: Add
id: 1640
time: '2026-01-27T23:27:00.379706+00:00'
- message: "\u0412 \u0447\u0430\u0442\u0430\u0445 \u0434\u0435\u043F\u0430\u0440\
\u0442\u0430\u043C\u0435\u043D\u0442\u043E\u0432 \u0431\u043E\u043B\u044C\u0448\
\u0435 \u043D\u0435 \u043F\u0440\u0438\u0445\u043E\u0434\u044F\u0442 \u0441\u043E\
\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043E\u0431 \u0434\u043E\u0431\u0430\
\u0432\u043B\u0435\u043D\u0438\u0438 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\
\u043A\u043E\u0432."
type: Tweak
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0434\u043E\u0431\u0430\u0432\
\u043B\u044F\u0442\u044C \u043B\u0438\u0447\u043D\u044B\u0435 \u0447\u0430\u0442\
\u044B \u0438 \u0433\u0440\u0443\u043F\u043F\u044B \u0432 \u0438\u0437\u0431\
\u0440\u0430\u043D\u043E\u0435 \u043F\u0440\u0438 \u043F\u043E\u043C\u043E\u0449\
\u0438 \u041F\u041A\u041C."
type: Tweak
- message: "\u0422\u0435\u043F\u0435\u0440\u044C \u0432 \u0441\u043F\u0438\u0441\
\u043A\u0435 \u043B\u0438\u0447\u043D\u044B\u0445 \u0447\u0430\u0442\u043E\u0432\
, \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F\u0445 \u0438\
\ \u0441\u043F\u0438\u0441\u043A\u0435 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\
\u043A\u043E\u0432 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\
\u0441\u044F \u0438\u043A\u043E\u043D\u043A\u0430 \u0441 \u0434\u043E\u043B\u0436\
\u043D\u043E\u0441\u0442\u044C\u044E."
type: Tweak
id: 1641
time: '2026-01-28T01:51:45.486583+00:00'