1655 lines
54 KiB
C#
1655 lines
54 KiB
C#
using System.Linq;
|
||
using System.Numerics;
|
||
using Content.Client._Sunrise.Messenger;
|
||
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 Content.Shared.StatusIcon;
|
||
using Robust.Shared.Timing;
|
||
using Content.Client._Sunrise.UserInterface.CustomControls;
|
||
|
||
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 EmojiPickerWindow? _emojiPickerDialog;
|
||
|
||
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||
|
||
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;
|
||
}
|
||
|
||
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)))
|
||
.ThenByDescending(u => state.UnreadCounts.TryGetValue(GetPersonalChatId(u.UserId), out var count) && count > 0)
|
||
.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))
|
||
.ThenByDescending(g => state.UnreadCounts.TryGetValue(g.GroupId, out var count) && count > 0)
|
||
.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.OrderByDescending(p => p.Value.Timestamp))
|
||
{
|
||
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)
|
||
{
|
||
_emojiPickerDialog.UpdateEmojiPickerContent();
|
||
return;
|
||
}
|
||
|
||
_emojiPickerDialog?.Close();
|
||
|
||
var dialog = new EmojiPickerWindow();
|
||
_emojiPickerDialog = dialog;
|
||
|
||
dialog.OnEmojiSelected += (emojiCode) =>
|
||
{
|
||
var currentText = MessageInput.Text;
|
||
MessageInput.Text = currentText + emojiCode;
|
||
MessageInput.CursorPosition = MessageInput.Text.Length;
|
||
};
|
||
|
||
dialog.OnClose += () => _emojiPickerDialog = null;
|
||
dialog.OpenCentered();
|
||
}
|
||
|
||
#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
|
||
}
|