fish-station/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUiFragment.xaml.cs
2026-01-30 06:28:52 +01:00

2099 lines
68 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Linq;
using System.Numerics;
using Content.Client._Sunrise.Messenger;
using Content.Client.Resources;
using Content.Shared._Sunrise.SunriseCCVars;
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
using Content.Shared._Sunrise.Messenger;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
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;
using Robust.Shared.Timing;
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class MessengerUiFragment : BoxContainer
{
public event Action<string?, string?, string, string?>? OnSendMessage;
public event Action<string>? OnCreateGroup;
public event Action<string, string>? OnAddToGroup;
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;
public event Action<string>? OnTogglePin;
public event Action? OnRequestPhotos;
private string? _currentChatId;
private MessengerUiState? _currentState;
private MessengerUiState? _previousState;
private bool _showMembersList;
private string? _currentGroupId;
private string _searchFilter = string.Empty;
private int _activeTab;
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;
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!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
private CreateGroupDialog? _createGroupDialog;
private AddUserDialog? _addUserDialog;
private DefaultWindow? _emojiPickerDialog;
private readonly List<string> _recentEmojis = new();
private readonly HashSet<string> _favoriteEmojis = new();
private BoxContainer? _emojiPickerContentContainer;
private ClientEmojiSystem EmojiSystem => _entitySystemManager.GetEntitySystem<ClientEmojiSystem>();
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
private static readonly Type[] MessageTagsAllowed =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(EmojiTag),
];
public MessengerUiFragment()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
VerticalExpand = true;
SendButton.OnPressed += _ => SendMessage();
MessageInput.OnTextEntered += _ => SendMessage();
CreateGroupButton.OnPressed += _ => ShowCreateGroupDialog();
ToggleMembersButton.OnPressed += _ => ToggleMembersList();
EmojiButton.OnPressed += _ => ShowEmojiPicker();
PhotoButton.OnPressed += _ => RequestPhotos();
SearchInput.OnTextChanged += OnSearchTextChanged;
PersonalChatsTab.OnPressed += _ => SwitchTab(0);
GroupChatsTab.OnPressed += _ => SwitchTab(1);
InvitesTab.OnPressed += _ => SwitchTab(2);
MuteCheckBox.OnToggled += OnMuteToggled;
PersonalChatsTab.Pressed = true;
MessagesContainer.OnScrolled += OnMessagesScrolled;
LoadSavedEmojis();
}
private void OnMessagesScrolled()
{
UpdateScrollBottomState();
}
private void UpdateScrollBottomState()
{
if (MessagesList == null || MessagesContainer == null)
return;
var vScroll = MessagesContainer.VScroll;
var containerHeight = MessagesContainer.Height;
var contentHeight = MessagesList.Height;
if (contentHeight <= containerHeight)
{
_isScrolledToBottom = true;
return;
}
var maxScroll = contentHeight - containerHeight;
var threshold = 50f;
_isScrolledToBottom = vScroll >= maxScroll - threshold;
}
public void UpdateState(MessengerUiState state)
{
_previousState = _currentState;
_currentState = state;
if (!state.ServerAvailable)
{
StatusLabel.Text = Loc.GetString("messenger-status-disconnected");
StatusLabel.Modulate = Color.Red;
}
else if (!state.IsRegistered)
{
StatusLabel.Text = Loc.GetString("messenger-status-connecting");
StatusLabel.Modulate = Color.Yellow;
}
else
{
StatusLabel.Text = Loc.GetString("messenger-status-connected");
StatusLabel.Modulate = Color.Green;
}
var canInteract = state.ServerAvailable && state.IsRegistered;
CreateGroupButton.Disabled = !canInteract;
CreateGroupButton.Visible = _activeTab == 1;
var hasChatSelected = _currentChatId != null;
InputContainer.Visible = hasChatSelected;
MessageInput.Editable = canInteract && hasChatSelected;
SendButton.Disabled = !canInteract || !hasChatSelected;
EmojiButton.Disabled = !canInteract || !hasChatSelected;
PhotoButton.Disabled = !canInteract || !hasChatSelected;
if (state.PhotoGallery != null && state.PhotoGallery.Count > 0)
{
ShowPhotoPicker(state.PhotoGallery);
}
var savedChatId = _currentChatId;
var savedGroupId = _currentGroupId;
var savedShowMembers = _showMembersList;
UpdateChatsList(state);
if (savedChatId != null)
{
_currentChatId = savedChatId;
_currentGroupId = savedGroupId;
_showMembersList = savedShowMembers;
string? chatName = null;
if (state.Groups.Any(g => g.GroupId == savedChatId))
{
var group = state.Groups.FirstOrDefault(g => g.GroupId == savedChatId);
chatName = group?.Name;
}
else if (savedChatId.StartsWith("personal_"))
{
var parts = savedChatId.Split('_');
if (parts.Length >= 3 && state.CurrentUserId != null)
{
var userId1 = parts[1];
var userId2 = parts[2];
var otherUserId = userId1 == state.CurrentUserId ? userId2 : userId1;
var user = state.Users.FirstOrDefault(u => u.UserId == otherUserId);
chatName = user?.Name;
}
}
if (chatName != null)
{
ChatNameLabel.Text = chatName;
ChatNameLabel.HorizontalExpand = true;
ChatNameLabel.MaxWidth = 230;
ChatNameLabel.HorizontalAlignment = HAlignment.Left;
ChatNameLabel.Align = Label.AlignMode.Left;
ChatNameLabel.RectClipContent = true;
}
var isGroup = state.Groups.Any(g => g.GroupId == savedChatId);
ToggleMembersButton.Visible = isGroup;
MembersContainer.Visible = _showMembersList && isGroup;
}
if (_currentChatId != null && state.MessageHistory.TryGetValue(_currentChatId, out var messages))
{
messages = messages.OrderBy(m => m.Timestamp)
.ThenBy(m => m.SenderId)
.ThenBy(m => m.Content)
.ToList();
if (messages.Count > MaxDisplayedMessages)
{
messages = messages.Skip(messages.Count - MaxDisplayedMessages).ToList();
}
var wasChatChanged = savedChatId != _currentChatId;
if (wasChatChanged)
{
_lastMessageCount = 0;
MessagesList.RemoveAllChildren();
}
var needsUpdate = MessagesList.ChildCount == 0 || messages.Count != _lastMessageCount;
if (!needsUpdate && _currentChatId != null && !_currentChatId.StartsWith("personal_"))
{
if (state.MessageHistory.TryGetValue(_currentChatId, out var newMessages))
{
if (_previousState?.MessageHistory.TryGetValue(_currentChatId, out var oldMessages) == true)
{
var oldMessageIds = new HashSet<long>(oldMessages.Select(m => m.MessageId));
var hasNewMessages = newMessages.Any(m => !oldMessageIds.Contains(m.MessageId));
if (hasNewMessages)
{
needsUpdate = true;
}
}
else if (newMessages.Count > 0)
{
needsUpdate = true;
}
}
}
if (!needsUpdate && _currentChatId != null && _currentChatId.StartsWith("personal_") && _currentState?.CurrentUserId != null)
{
if (state.MessageHistory.TryGetValue(_currentChatId, out var newMessages))
{
if (HasMessageStatusChanged(newMessages, _currentState.CurrentUserId))
{
needsUpdate = true;
}
}
}
if (needsUpdate)
{
UpdateMessages(messages);
}
}
else if (_currentChatId == null)
{
_lastMessageCount = 0;
MessagesList.RemoveAllChildren();
}
else if (_currentChatId != null)
{
OnRequestMessages?.Invoke(_currentChatId);
}
UpdateChatsList(state);
UpdateTabButtons(state);
if (_showMembersList && _currentGroupId != null)
{
UpdateMembersList();
}
}
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;
}
}
}
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);
}
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
{
_searchFilter = args.Text;
_lastUserIds = null;
_lastGroupIds = null;
_lastUnreadCounts = null;
_lastActiveTab = -1;
if (_currentState != null)
{
UpdateChatsList(_currentState);
}
}
private void OnMuteToggled(BaseButton.ButtonToggledEventArgs args)
{
if (_currentChatId == null || _currentState == null)
return;
var isMuted = args.Pressed;
OnToggleMute?.Invoke(_currentChatId, isMuted);
}
private void UpdateChatsList(MessengerUiState state)
{
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;
if (!needsUpdate)
{
UpdateChatButtonsAppearance(state);
return;
}
_lastUserIds = currentUserIds;
_lastGroupIds = currentGroupIds;
_lastUnreadCounts = new Dictionary<string, int>(state.UnreadCounts);
_lastPinnedChats = new HashSet<string>(currentPinnedChats);
_lastActiveTab = _activeTab;
ChatsContainer.RemoveAllChildren();
if (!state.ServerAvailable || !state.IsRegistered)
{
var noConnectionLabel = new Label
{
Text = state.ServerAvailable
? Loc.GetString("messenger-status-connecting")
: Loc.GetString("messenger-status-disconnected"),
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(4)
};
ChatsContainer.AddChild(noConnectionLabel);
return;
}
var searchLower = _searchFilter.ToLowerInvariant();
if (_activeTab == 0)
{
UpdatePersonalChatsList(state, searchLower);
}
else if (_activeTab == 1)
{
UpdateGroupChatsList(state, searchLower);
}
else if (_activeTab == 2)
{
UpdateInvitesList(state);
}
}
/// <summary>
/// Обновляет список личных чатов
/// </summary>
private void UpdatePersonalChatsList(MessengerUiState state, string searchFilter)
{
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;
if (!string.IsNullOrEmpty(searchFilter) && !userName.ToLowerInvariant().Contains(searchFilter))
continue;
var buttonContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Margin = new Thickness(2)
};
var button = CreatePersonalChatButton(user, state);
buttonContainer.AddChild(button);
ChatsContainer.AddChild(buttonContainer);
}
}
/// <summary>
/// Обновляет список групповых чатов
/// </summary>
private void UpdateGroupChatsList(MessengerUiState state, string searchFilter)
{
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 groupName = group.Name;
if (!string.IsNullOrEmpty(searchFilter) && !groupName.ToLowerInvariant().Contains(searchFilter))
continue;
var groupContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Margin = new Thickness(2)
};
var button = CreateGroupChatButton(group, state);
groupContainer.AddChild(button);
ChatsContainer.AddChild(groupContainer);
}
}
/// <summary>
/// Обновляет список приглашений
/// </summary>
private void UpdateInvitesList(MessengerUiState state)
{
if (state.ActiveInvites.Count > 0)
{
foreach (var invite in state.ActiveInvites)
{
var invitePanel = CreateInvitePanel(invite);
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)
{
if (counts1.Count != counts2.Count)
return false;
foreach (var kvp in counts1)
{
if (!counts2.TryGetValue(kvp.Key, out var value) || value != kvp.Value)
return false;
}
return true;
}
private void UpdateChatButtonsAppearance(MessengerUiState state)
{
foreach (var child in ChatsContainer.Children)
{
if (child is BoxContainer container && container.ChildCount > 0)
{
var button = container.Children.FirstOrDefault() as Button;
if (button == null || button.ChildCount == 0)
continue;
var textContainer = button.Children.FirstOrDefault() as BoxContainer;
if (textContainer == null)
continue;
var nameLabel = textContainer.Children.FirstOrDefault() as Label;
if (nameLabel == null)
continue;
var chatName = nameLabel.Text;
string? chatId = null;
if (state.Users.Any(u => u.Name == chatName && u.UserId != state.CurrentUserId))
{
var user = state.Users.First(u => u.Name == chatName && u.UserId != state.CurrentUserId);
chatId = GetPersonalChatId(user.UserId);
}
else if (state.Groups.Any(g => g.Name == chatName))
{
chatId = state.Groups.First(g => g.Name == chatName).GroupId;
}
if (chatId == null)
continue;
var unreadCount = state.UnreadCounts.TryGetValue(chatId, out var count) ? count : 0;
var hasUnread = unreadCount > 0;
button.ModulateSelfOverride = hasUnread ? Color.Red : Color.White;
var existingCounter = textContainer.Children.OfType<Label>()
.FirstOrDefault(l => l.StyleClasses.Contains("Bold") &&
(l.ModulateSelfOverride == Color.White ||
l.ModulateSelfOverride == default && l.Modulate == Color.White));
if (hasUnread)
{
if (existingCounter == null)
{
var counterLabel = new Label
{
Text = $"({unreadCount})",
Align = Label.AlignMode.Left,
StyleClasses = { "Bold" },
HorizontalExpand = false,
MinSize = new Vector2(40, 0)
};
counterLabel.ModulateSelfOverride = Color.White;
textContainer.AddChild(counterLabel);
nameLabel.HorizontalExpand = true;
}
else
{
existingCounter.Text = $"({unreadCount})";
existingCounter.MinSize = new Vector2(40, 0);
existingCounter.ModulateSelfOverride = Color.White;
}
}
else
{
if (existingCounter != null)
{
textContainer.RemoveChild(existingCounter);
nameLabel.HorizontalExpand = true;
}
}
}
}
}
private void SelectChat(string chatId, string chatName)
{
if (_currentChatId == chatId)
return;
_lastMessageCount = 0;
MessagesList.RemoveAllChildren();
_isScrolledToBottom = true;
_currentChatId = chatId;
ChatNameLabel.Text = chatName;
ChatNameLabel.HorizontalExpand = true;
ChatNameLabel.MaxWidth = 230;
ChatNameLabel.HorizontalAlignment = HAlignment.Left;
ChatNameLabel.Align = Label.AlignMode.Left;
ChatNameLabel.RectClipContent = true;
var isGroup = _currentState?.Groups.Any(g => g.GroupId == chatId) ?? false;
_currentGroupId = isGroup ? chatId : null;
OnRequestMessages?.Invoke(chatId);
if (_currentState != null && _currentState.MessageHistory.TryGetValue(chatId, out var messages))
{
UpdateMessages(messages);
}
if (_currentState != null)
{
UpdateChatsList(_currentState);
}
ToggleMembersButton.Visible = isGroup;
if (!isGroup)
{
_showMembersList = false;
MembersContainer.Visible = false;
}
MessagesContainer.Visible = !_showMembersList;
var canInteract = _currentState?.ServerAvailable == true && _currentState?.IsRegistered == true;
if (_currentState != null)
{
var isMuted = isGroup
? _currentState.MutedGroupChats.Contains(chatId)
: _currentState.MutedPersonalChats.Contains(chatId);
MuteCheckBox.Pressed = isMuted;
MuteCheckBox.Visible = true;
}
else
{
MuteCheckBox.Visible = false;
}
InputContainer.Visible = true;
EmojiButton.Disabled = !canInteract;
if (isGroup)
{
UpdateMembersList();
}
MessageInput.Editable = canInteract;
SendButton.Disabled = !canInteract;
}
private void ToggleMembersList()
{
_showMembersList = !_showMembersList;
MembersContainer.Visible = _showMembersList;
MessagesContainer.Visible = !_showMembersList;
ToggleMembersButton.Text = _showMembersList ? "✖" : "≡";
ToggleMembersButton.ToolTip = _showMembersList
? Loc.GetString("messenger-members-toggle-hide")
: Loc.GetString("messenger-members-toggle-show");
if (_showMembersList)
{
UpdateMembersList();
}
}
private void UpdateMembersList()
{
if (_currentState == null || _currentGroupId == null)
return;
MembersList.RemoveAllChildren();
var group = _currentState.Groups.FirstOrDefault(g => g.GroupId == _currentGroupId);
if (group == null)
return;
var header = new Label
{
Text = Loc.GetString("messenger-members-header"),
StyleClasses = { "Bold" },
Margin = new Thickness(2)
};
MembersList.AddChild(header);
var members = group.Members;
var isOwner = !string.IsNullOrEmpty(group.OwnerId) && group.OwnerId == _currentState.CurrentUserId;
var isUserGroup = group.Type == MessengerGroupType.UserCreated;
if (members.Count > 0)
{
foreach (var memberId in members)
{
var user = _currentState?.Users.FirstOrDefault(u => u.UserId == memberId);
var userName = user?.Name ?? memberId;
var isMemberOwner = memberId == group.OwnerId;
var memberPanel = CreateStyledPanel(
Color.FromHex("#25252A"),
Color.FromHex("#3D4059"));
var memberContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true
};
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)
{
CreateJobIconRect(user.JobIconId, memberContainer);
}
var tooltipText = isMemberOwner
? Loc.GetString("messenger-member-owner", ("name", userName))
: userName;
var memberLabel = new Label
{
Text = userName,
HorizontalExpand = true,
ClipText = true,
ToolTip = tooltipText
};
memberContainer.AddChild(memberLabel);
if (isUserGroup && isOwner && !isMemberOwner && _currentState != null && _currentState.ServerAvailable && _currentState.IsRegistered)
{
var removeButton = new Button
{
Text = "✖",
SetWidth = 25,
ToolTip = Loc.GetString("messenger-remove-member-tooltip")
};
var userIdToRemove = memberId;
removeButton.OnPressed += _ => OnRemoveFromGroup?.Invoke(_currentGroupId, userIdToRemove);
memberContainer.AddChild(removeButton);
}
memberPanel.AddChild(memberContainer);
MembersList.AddChild(memberPanel);
}
}
else
{
var noMembersLabel = new Label
{
Text = Loc.GetString("messenger-no-members"),
Margin = new Thickness(2, 1)
};
MembersList.AddChild(noMembersLabel);
}
if (isUserGroup && _currentState != null && _currentState.ServerAvailable && _currentState.IsRegistered)
{
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 (ShouldClearMessagesList(messages, hasNewMessages))
{
MessagesList.RemoveAllChildren();
_lastMessageCount = 0;
}
if (!ShouldUpdateMessagesList(messages, hasNewMessages))
{
return;
}
AddNewMessages(messages);
RemoveDeletedMessages(messages);
_lastMessageCount = messages.Count;
var shouldScroll = (hasNewMessages && wasAtBottom || _lastMessageCount == 0 &&
messages.Count > 0 || hasNewMessages && _isScrolledToBottom);
if (shouldScroll)
{
ScrollToBottom();
}
else
{
UpdateScrollBottomState();
}
}
/// <summary>
/// Проверяет, нужно ли очистить список сообщений
/// </summary>
private bool ShouldClearMessagesList(List<MessengerMessage> messages, bool hasNewMessages)
{
if (_lastMessageCount == 0 || messages.Count < _lastMessageCount)
return true;
if (messages.Count > 0 && _lastMessageCount > 0)
{
var firstDisplayedId = GetFirstDisplayedMessageId();
if (firstDisplayedId.HasValue && messages[0].MessageId != firstDisplayedId.Value)
return true;
}
if (!hasNewMessages && messages.Count == _lastMessageCount && MessagesList.ChildCount > 0)
{
var isPersonalChat = _currentChatId != null && _currentChatId.StartsWith("personal_");
if (isPersonalChat && _currentState?.CurrentUserId != null && _currentChatId != null)
{
return HasMessageStatusChanged(messages, _currentState.CurrentUserId);
}
}
return false;
}
/// <summary>
/// Проверяет, нужно ли обновить список сообщений
/// </summary>
private bool ShouldUpdateMessagesList(List<MessengerMessage> messages, bool hasNewMessages)
{
if (hasNewMessages)
return true;
if (messages.Count != _lastMessageCount)
return true;
if (MessagesList.ChildCount == 0 && messages.Count > 0)
return true;
var isPersonalChat = _currentChatId != null && _currentChatId.StartsWith("personal_");
if (isPersonalChat && _currentState?.CurrentUserId != null && _currentChatId != null)
{
return HasMessageStatusChanged(messages, _currentState.CurrentUserId);
}
return false;
}
/// <summary>
/// Добавляет новые сообщения в список
/// </summary>
private void AddNewMessages(List<MessengerMessage> messages)
{
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);
}
}
/// <summary>
/// Удаляет сообщения, которых больше нет в списке
/// </summary>
private void RemoveDeletedMessages(List<MessengerMessage> messages)
{
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);
}
}
/// <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;
if (string.IsNullOrWhiteSpace(messageText) || _currentChatId == null)
return;
string? recipientId = null;
string? groupId = null;
var chatId = _currentChatId;
if (chatId.StartsWith("personal_"))
{
var parts = chatId.Split('_');
if (parts.Length >= 3 && _currentState?.CurrentUserId != null)
{
var userId1 = parts[1];
var userId2 = parts[2];
recipientId = userId1 == _currentState.CurrentUserId ? userId2 : userId1;
}
}
else if (_currentState?.Groups.Any(g => g.GroupId == chatId) ?? false)
{
groupId = chatId;
}
MessageInput.Clear();
OnSendMessage?.Invoke(recipientId, groupId, messageText, null);
ScrollToBottom();
}
private void SendPhoto(string imagePath)
{
if (_currentChatId == null)
return;
string? recipientId = null;
string? groupId = null;
if (_currentChatId.StartsWith("personal_"))
{
var parts = _currentChatId.Split('_');
if (parts.Length >= 3 && _currentState?.CurrentUserId != null)
{
var userId1 = parts[1];
var userId2 = parts[2];
recipientId = userId1 == _currentState.CurrentUserId ? userId2 : userId1;
}
}
else if (_currentState?.Groups.Any(g => g.GroupId == _currentChatId) ?? false)
{
groupId = _currentChatId;
}
OnSendMessage?.Invoke(recipientId, groupId, string.Empty, imagePath);
}
private void RequestPhotos()
{
if (_currentChatId == null)
return;
OnRequestPhotos?.Invoke();
}
private DefaultWindow? _photoPickerDialog;
private void ShowPhotoPicker(Dictionary<string, PhotoMetadata> photos)
{
if (_photoPickerDialog != null && _photoPickerDialog.IsOpen)
return;
var dialog = new DefaultWindow
{
Title = Loc.GetString("messenger-photo-picker-title"),
MinSize = new Vector2(560, 620),
};
_photoPickerDialog = dialog;
var scroll = new ScrollContainer
{
HorizontalExpand = true,
VerticalExpand = true
};
var grid = new GridContainer
{
Columns = 2,
HorizontalExpand = true
};
foreach (var (id, metadata) in photos)
{
var photoControl = new PhotoItemControl(id, metadata,
_netTexturesManager,
_resourceCache,
_gameTiming);
photoControl.MinSize = new Vector2(120, 120);
photoControl.OnPressed += _ =>
{
SendPhoto(metadata.ImagePath);
dialog.Close();
};
grid.AddChild(photoControl);
}
scroll.AddChild(grid);
dialog.Contents.AddChild(scroll);
dialog.OnClose += () => _photoPickerDialog = null;
dialog.OpenCentered();
}
private string GetPersonalChatId(string userId)
{
if (_currentState?.CurrentUserId == null)
return $"personal_{userId}_unknown";
var ids = new[] { userId, _currentState.CurrentUserId }.OrderBy(x => x).ToArray();
return $"personal_{ids[0]}_{ids[1]}";
}
private void ShowCreateGroupDialog()
{
_createGroupDialog?.Close();
var dialog = new CreateGroupDialog();
_createGroupDialog = dialog;
dialog.OnCreate += (groupName) =>
{
OnCreateGroup?.Invoke(groupName);
};
dialog.OnClose += () => _createGroupDialog = null;
dialog.OpenCentered();
}
private void OnAddMemberButtonPressed(BaseButton.ButtonEventArgs args)
{
if (_currentGroupId != null && _currentState != null)
{
var group = _currentState.Groups.FirstOrDefault(g => g.GroupId == _currentGroupId);
if (group != null)
{
ShowAddUserToGroupDialog(_currentGroupId, group.Name);
}
}
}
private void ShowAddUserToGroupDialog(string groupId, string groupName)
{
if (_currentState == null)
return;
var group = _currentState.Groups.FirstOrDefault(g => g.GroupId == groupId);
if (group == null)
return;
var existingMembers = group.Members;
var availableUsers = _currentState.Users
.Where(u =>
u.UserId != _currentState.CurrentUserId &&
!existingMembers.Contains(u.UserId))
.ToList();
_addUserDialog?.Close();
if (availableUsers.Count == 0)
{
var noUsersDialog = new DefaultWindow
{
Title = Loc.GetString("messenger-add-user-title"),
MinSize = new Vector2(250, 100)
};
var container = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
Margin = new Thickness(8)
};
var label = new Label
{
Text = Loc.GetString("messenger-no-users-available"),
HorizontalExpand = true
};
container.AddChild(label);
noUsersDialog.Contents.AddChild(container);
noUsersDialog.OpenCentered();
return;
}
var dialog = new AddUserDialog();
var dialogTitle = Loc.GetString("messenger-invite-user-to-group-title", ("groupName", groupName));
dialog.SetTitle(dialogTitle);
_addUserDialog = dialog;
dialog.SetAvailableUsers(availableUsers);
dialog.OnUserSelected += (userId) =>
{
OnAddToGroup?.Invoke(groupId, userId);
};
dialog.OnClose += () => _addUserDialog = null;
dialog.OpenCentered();
}
private void ShowEmojiPicker()
{
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
{
UpdateEmojiPickerContent();
return;
}
var emojiSystem = EmojiSystem;
var spriteSystem = GetSpriteSystem();
_emojiPickerDialog?.Close();
var dialog = new DefaultWindow
{
Title = Loc.GetString("messenger-emoji-picker-title"),
MinSize = new Vector2(445, 400),
Resizable = false,
};
_emojiPickerDialog = dialog;
var container = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
Margin = new Thickness(8)
};
var scrollContainer = new ScrollContainer
{
HorizontalExpand = true,
VerticalExpand = true,
HScrollEnabled = false,
ReserveScrollbarSpace = true
};
var contentContainer = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 5)
};
_emojiPickerContentContainer = contentContainer;
BuildEmojiPickerContent(contentContainer, emojiSystem, spriteSystem);
scrollContainer.AddChild(contentContainer);
container.AddChild(scrollContainer);
dialog.Contents.AddChild(container);
dialog.OnClose += () =>
{
_emojiPickerDialog = null;
_emojiPickerContentContainer = null;
};
dialog.OpenCentered();
}
private void UpdateEmojiPickerContent()
{
if (_emojiPickerContentContainer == null)
return;
var emojiSystem = EmojiSystem;
var spriteSystem = GetSpriteSystem();
_emojiPickerContentContainer.RemoveAllChildren();
BuildEmojiPickerContent(_emojiPickerContentContainer, emojiSystem, spriteSystem);
}
private void BuildEmojiPickerContent(BoxContainer contentContainer, ClientEmojiSystem emojiSystem, SpriteSystem spriteSystem)
{
var allEmojis = emojiSystem.GetAllEmojis().ToList();
var emojiDict = allEmojis.ToDictionary(e => e.Code, e => e);
BuildRecentEmojisSection(contentContainer, emojiDict, spriteSystem);
BuildFavoriteEmojisSection(contentContainer, allEmojis, spriteSystem);
BuildAllEmojisSection(contentContainer, allEmojis, spriteSystem);
}
/// <summary>
/// Создает стиль панели для секций эмодзи
/// </summary>
private StyleBoxTexture CreateEmojiPanelStyle()
{
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
var panelStyle = new StyleBoxTexture
{
Texture = panelTex
};
panelStyle.SetPatchMargin(StyleBox.Margin.All, 5);
panelStyle.SetContentMarginOverride(StyleBox.Margin.All, 8);
panelStyle.Modulate = Color.FromHex("#2F2F35");
return panelStyle;
}
/// <summary>
/// Создает секцию недавних эмодзи
/// </summary>
private void BuildRecentEmojisSection(BoxContainer contentContainer, Dictionary<string, EmojiPrototype> emojiDict, SpriteSystem spriteSystem)
{
var recentPanel = new PanelContainer
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 10)
};
recentPanel.PanelOverride = CreateEmojiPanelStyle();
var recentSection = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true
};
var recentLabel = new Label
{
Text = Loc.GetString("messenger-emoji-recent-title"),
StyleClasses = { "Bold" },
Margin = new Thickness(0, 0, 0, 4)
};
recentSection.AddChild(recentLabel);
if (_recentEmojis.Count > 0)
{
var recentContainer = new GridContainer
{
Columns = 5,
HorizontalExpand = true
};
var recentToShow = _recentEmojis.TakeLast(5).Reverse().ToList();
foreach (var emojiCode in recentToShow)
{
if (emojiDict.TryGetValue(emojiCode, out var emoji))
{
var button = CreateEmojiButton(emoji, spriteSystem, false);
recentContainer.AddChild(button);
}
}
recentSection.AddChild(recentContainer);
}
else
{
var hintLabel = new Label
{
Text = Loc.GetString("messenger-emoji-recent-empty-hint"),
StyleClasses = { "LabelSubText" }
};
recentSection.AddChild(hintLabel);
}
recentPanel.AddChild(recentSection);
contentContainer.AddChild(recentPanel);
}
/// <summary>
/// Создает секцию избранных эмодзи
/// </summary>
private void BuildFavoriteEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis, SpriteSystem spriteSystem)
{
var favoritePanel = new PanelContainer
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 10)
};
favoritePanel.PanelOverride = CreateEmojiPanelStyle();
var favoriteSection = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true
};
var favoriteLabel = new Label
{
Text = Loc.GetString("messenger-emoji-favorite-title"),
StyleClasses = { "Bold" },
Margin = new Thickness(0, 0, 0, 4)
};
favoriteSection.AddChild(favoriteLabel);
if (_favoriteEmojis.Count > 0)
{
var favoriteContainer = new GridContainer
{
Columns = 5,
HorizontalExpand = true
};
var favoriteEmojisList = allEmojis.Where(e => _favoriteEmojis.Contains(e.Code)).ToList();
foreach (var emoji in favoriteEmojisList)
{
var button = CreateEmojiButton(emoji, spriteSystem, true);
favoriteContainer.AddChild(button);
}
favoriteSection.AddChild(favoriteContainer);
}
else
{
var hintFavoriteLabel = new Label
{
Text = Loc.GetString("messenger-emoji-favorite-hint"),
StyleClasses = { "LabelSubText" },
Margin = new Thickness(0, 4, 0, 0)
};
favoriteSection.AddChild(hintFavoriteLabel);
}
favoritePanel.AddChild(favoriteSection);
contentContainer.AddChild(favoritePanel);
}
/// <summary>
/// Создает секцию всех эмодзи
/// </summary>
private void BuildAllEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis, SpriteSystem spriteSystem)
{
var allPanel = new PanelContainer
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 0)
};
allPanel.PanelOverride = CreateEmojiPanelStyle();
var allSection = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true
};
var allLabel = new Label
{
Text = Loc.GetString("messenger-emoji-all-title"),
StyleClasses = { "Bold" },
Margin = new Thickness(0, 0, 0, 4)
};
allSection.AddChild(allLabel);
var allEmojisContainer = new GridContainer
{
Columns = 5,
HorizontalExpand = true
};
var nonFavoriteEmojis = allEmojis.Where(e => !_favoriteEmojis.Contains(e.Code)).ToList();
foreach (var emoji in nonFavoriteEmojis)
{
var button = CreateEmojiButton(emoji, spriteSystem, false);
allEmojisContainer.AddChild(button);
}
allSection.AddChild(allEmojisContainer);
allPanel.AddChild(allSection);
contentContainer.AddChild(allPanel);
}
private Button CreateEmojiButton(EmojiPrototype emoji, SpriteSystem spriteSystem, bool isFavorite)
{
var emojiButton = new Button
{
MinSize = new Vector2(75, 75),
MaxSize = new Vector2(75, 75),
ToolTip = emoji.Code
};
try
{
var spriteSpec = new SpriteSpecifier.Rsi(new ResPath(emoji.SpritePath), emoji.SpriteState);
var state = spriteSystem.RsiStateLike(spriteSpec);
emojiButton.Label.Visible = false;
if (state.IsAnimated)
{
var animatedRect = new AnimatedTextureRect
{
SetWidth = 65,
SetHeight = 65,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center
};
animatedRect.SetFromSpriteSpecifier(spriteSpec);
animatedRect.DisplayRect.HorizontalExpand = true;
animatedRect.DisplayRect.VerticalExpand = true;
animatedRect.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
emojiButton.AddChild(animatedRect);
}
else
{
var texture = spriteSystem.Frame0(spriteSpec);
var textureRect = new TextureRect
{
Texture = texture,
SetWidth = 45,
SetHeight = 45,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Stretch = TextureRect.StretchMode.KeepAspectCentered
};
emojiButton.AddChild(textureRect);
}
}
catch
{
emojiButton.Text = emoji.Code;
}
var emojiCode = emoji.Code;
emojiButton.OnPressed += _ =>
{
var currentText = MessageInput.Text;
MessageInput.Text = currentText + emojiCode;
MessageInput.CursorPosition = MessageInput.Text.Length;
AddToRecentEmojis(emojiCode);
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
{
UpdateEmojiPickerContent();
}
};
emojiButton.OnKeyBindDown += args =>
{
if (args.Function == EngineKeyFunctions.UIRightClick)
{
args.Handle();
ToggleFavoriteEmoji(emojiCode);
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
{
UpdateEmojiPickerContent();
}
}
};
return emojiButton;
}
private void AddToRecentEmojis(string emojiCode)
{
_recentEmojis.Remove(emojiCode);
_recentEmojis.Add(emojiCode);
if (_recentEmojis.Count > 5)
{
_recentEmojis.RemoveAt(0);
}
SaveRecentEmojis();
}
private void ToggleFavoriteEmoji(string emojiCode)
{
if (!_favoriteEmojis.Add(emojiCode))
{
_favoriteEmojis.Remove(emojiCode);
}
SaveFavoriteEmojis();
}
private void LoadSavedEmojis()
{
HashSet<string>? allEmojis = null;
try
{
allEmojis = EmojiSystem.GetAllEmojis().Select(e => e.Code).ToHashSet();
}
catch
{
// ignored
}
try
{
var recentEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerRecentEmojis);
if (!string.IsNullOrWhiteSpace(recentEmojisStr))
{
var emojiCodes = recentEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
_recentEmojis.Clear();
foreach (var code in emojiCodes)
{
if (string.IsNullOrWhiteSpace(code))
continue;
if (allEmojis != null && !allEmojis.Contains(code))
continue;
_recentEmojis.Add(code);
if (_recentEmojis.Count >= 5)
break;
}
}
}
catch
{
_recentEmojis.Clear();
}
try
{
var favoriteEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerFavoriteEmojis);
if (!string.IsNullOrWhiteSpace(favoriteEmojisStr))
{
var emojiCodes = favoriteEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
_favoriteEmojis.Clear();
foreach (var code in emojiCodes)
{
if (string.IsNullOrWhiteSpace(code))
continue;
if (allEmojis != null && !allEmojis.Contains(code))
continue;
_favoriteEmojis.Add(code);
}
}
}
catch
{
_favoriteEmojis.Clear();
}
}
private void SaveRecentEmojis()
{
try
{
var emojiCodesStr = string.Join(",", _recentEmojis);
_configurationManager.SetCVar(SunriseCCVars.MessengerRecentEmojis, emojiCodesStr);
_configurationManager.SaveToFile();
}
catch
{
// ignored
}
}
private void SaveFavoriteEmojis()
{
try
{
var emojiCodesStr = string.Join(",", _favoriteEmojis);
_configurationManager.SetCVar(SunriseCCVars.MessengerFavoriteEmojis, emojiCodesStr);
_configurationManager.SaveToFile();
}
catch
{
// ignored
}
}
#region UI Helper Methods
/// <summary>
/// Создает кнопку чата с общими элементами
/// </summary>
private Button CreateChatButton(string chatId, string chatName, bool isPinned, int unreadCount,
ProtoId<JobIconPrototype>? jobIconId, Action<string, string> onSelect, Action<string>? onTogglePin)
{
var button = new Button
{
HorizontalExpand = true,
ClipText = false,
TextAlign = Label.AlignMode.Left,
ToolTip = chatName
};
var textContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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 (jobIconId != null)
{
CreateJobIconRect(jobIconId, textContainer);
}
var nameLabel = new Label
{
Text = chatName,
HorizontalExpand = true,
ClipText = true,
Align = Label.AlignMode.Left
};
textContainer.AddChild(nameLabel);
if (unreadCount > 0)
{
var counterLabel = CreateUnreadCounterLabel(unreadCount);
textContainer.AddChild(counterLabel);
button.ModulateSelfOverride = Color.Red;
}
button.AddChild(textContainer);
button.OnPressed += _ => onSelect(chatId, chatName);
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);
}
};
return button;
}
/// <summary>
/// Создает кнопку для личного чата
/// </summary>
private Button CreatePersonalChatButton(MessengerUser user, MessengerUiState state)
{
var chatId = GetPersonalChatId(user.UserId);
var isPinned = state.PinnedChats.Contains(chatId);
var unreadCount = state.UnreadCounts.TryGetValue(chatId, out var count) ? count : 0;
return CreateChatButton(
chatId,
user.Name,
isPinned,
unreadCount,
user.JobIconId,
SelectChat,
OnTogglePin != null ? OnTogglePin.Invoke : null
);
}
/// <summary>
/// Создает кнопку для группового чата
/// </summary>
private Button CreateGroupChatButton(MessengerGroup group, MessengerUiState state)
{
var isPinned = state.PinnedChats.Contains(group.GroupId);
var unreadCount = state.UnreadCounts.TryGetValue(group.GroupId, out var count) ? count : 0;
return CreateChatButton(
group.GroupId,
group.Name,
isPinned,
unreadCount,
null,
SelectChat,
OnTogglePin != null ? OnTogglePin.Invoke : null
);
}
/// <summary>
/// Создает панель приглашения
/// </summary>
private PanelContainer CreateInvitePanel(MessengerGroupInvite invite)
{
var invitePanel = CreateStyledPanel(
Color.FromHex("#25252A"),
Color.FromHex("#3D4059"));
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);
return invitePanel;
}
/// <summary>
/// Создает TextureRect с иконкой работы и добавляет его в контейнер
/// </summary>
private void CreateJobIconRect(ProtoId<JobIconPrototype>? jobIconId, Control container)
{
if (jobIconId != null && _prototypeManager.TryIndex(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)
};
container.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)
};
container.AddChild(iconRect);
}
}
}
/// <summary>
/// Создает панель с общим стилем для приглашений и участников
/// </summary>
private PanelContainer CreateStyledPanel(Color backgroundColor, Color borderColor)
{
var panel = new PanelContainer
{
HorizontalExpand = true,
Margin = new Thickness(2, 2)
};
panel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = backgroundColor,
BorderColor = borderColor,
BorderThickness = new Thickness(1),
ContentMarginLeftOverride = 6,
ContentMarginRightOverride = 6,
ContentMarginTopOverride = 4,
ContentMarginBottomOverride = 4
};
return panel;
}
/// <summary>
/// Создает метку счетчика непрочитанных сообщений
/// </summary>
private Label CreateUnreadCounterLabel(int count)
{
var counterLabel = new Label
{
Text = $"({count})",
Align = Label.AlignMode.Left,
StyleClasses = { "Bold" },
HorizontalExpand = false,
MinSize = new Vector2(40, 0)
};
counterLabel.ModulateSelfOverride = Color.White;
return counterLabel;
}
/// <summary>
/// Прокручивает список сообщений до конца
/// </summary>
private void ScrollToBottom()
{
_userInterfaceManager.DeferAction(() =>
{
if (MessagesList == null || MessagesContainer == null)
return;
var contentHeight = MessagesList.Height;
var containerHeight = MessagesContainer.Height;
var maxScroll = Math.Max(0, contentHeight - containerHeight);
MessagesContainer.VScroll = maxScroll;
MessagesContainer.VScrollTarget = maxScroll;
_isScrolledToBottom = true;
});
}
/// <summary>
/// Проверяет, изменился ли статус прочтения сообщений
/// </summary>
private bool HasMessageStatusChanged(List<MessengerMessage> newMessages, string currentUserId)
{
if (_previousState?.MessageHistory.TryGetValue(_currentChatId ?? string.Empty, out var oldMessages) != true || oldMessages == null)
return false;
foreach (var newMsg in newMessages)
{
if (newMsg.SenderId == currentUserId && newMsg.RecipientId != null)
{
var oldMsg = oldMessages.FirstOrDefault(m =>
m.SenderId == newMsg.SenderId &&
Math.Abs(m.Timestamp.TotalSeconds - newMsg.Timestamp.TotalSeconds) < 0.001 &&
m.Content == newMsg.Content);
if (oldMsg != null && oldMsg.IsRead != newMsg.IsRead)
{
return true;
}
}
}
return false;
}
#endregion
}