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? OnSendMessage; public event Action? OnCreateGroup; public event Action? OnAddToGroup; public event Action? OnRemoveFromGroup; public event Action? OnRequestMessages; public event Action? OnToggleMute; public event Action? OnAcceptInvite; public event Action? OnDeclineInvite; public event Action? OnLeaveGroup; public event Action? OnDeleteMessage; public event Action? 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; /// /// Максимальное количество сообщений, отображаемых в UI (для оптимизации производительности) /// private const int MaxDisplayedMessages = 100; private Dictionary? _lastUnreadCounts; private HashSet? _lastUserIds; private HashSet? _lastGroupIds; private HashSet? _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 _recentEmojis = new(); private readonly HashSet _favoriteEmojis = new(); private BoxContainer? _emojiPickerContentContainer; private ClientEmojiSystem EmojiSystem => _entitySystemManager.GetEntitySystem(); private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem(); 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(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