Chairbender Chat (#3794)
* #272 restructure and restyle chat line edit section * #272 no arrow, actually change id on channel changer * #272 nice round chat channel picker * #272 add chat channel selection logic, and auto-select when a prefix is entered * #272 consistent width of chat channel btn * #272 only show admin channel filter if asay perms * #272 add tutorial info on chat prefixes * #272 added chat filter button * #272 added chat filter button * #272 WIP on filter popup * #272 fix filter popup pressed / unpressed logic * #272 fix filter popup positioning and layout * #272 WIP channel filter logic * #272 WIP channel filter logic * #272 WIP refactoring how chatbox / manager manages available filters and channels to send on * #272 WIP implementing filtering UI / logic and refactoring how chat UI is managed * #272 fix various bugs with new chat filter / selector logic * #272 remove outdated todos * #272 WIP working chat window resize * #272 bounded chatbox resizing * #272 alertUI moves with resized chat * #272 WIP making alertUI not be too large when changing size / UIScale * #272 WIP fixing window / uiscale adjustment * #272 WIP hacky approach for resizing, will try another approach * #272 implement hacky approach for bounded chat resize * #272 no resizing of lobby chat * #272 WIP adding unread marker to chat filters * #272 basic working unread chat message indicators * #272 WIP adding horizontal channel selector items * #272 horizontal channel selector popup * #272 workaround for chat selector staying highlighted when right clicking it while toggled * #272 workaround for chat selector staying highlighted when right clicking it while toggled * #272 wip trying to add tests for chatbox * #272 remove test, not really possible with current system * #272 merge latest * #272 merge latest * #272 fix csproj changes * It works if you disable the lobby * Fixes lobby chat * Adds more channel focusses * Channel cycler * Address review * Address nitpicks * Address more of the review * Fix chat post-viewport * Finalize review stuff Co-authored-by: chairbender <kwhipke1@gmail.com> Co-authored-by: ike709 <sparebytes@protonmail.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Administration;
|
||||
using Content.Client.GameObjects.Components.Observer;
|
||||
@@ -22,8 +23,6 @@ namespace Content.Client.Chat
|
||||
{
|
||||
internal sealed class ChatManager : IChatManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private IPlayerManager _playerManager = default!;
|
||||
|
||||
private struct SpeechBubbleData
|
||||
{
|
||||
public string Message;
|
||||
@@ -55,32 +54,62 @@ namespace Content.Client.Chat
|
||||
/// </summary>
|
||||
private int _maxMessageLength = 1000;
|
||||
|
||||
private const char ConCmdSlash = '/';
|
||||
private const char OOCAlias = '[';
|
||||
private const char MeAlias = '@';
|
||||
private const char AdminChatAlias = ']';
|
||||
public const char ConCmdSlash = '/';
|
||||
public const char OOCAlias = '[';
|
||||
public const char MeAlias = '@';
|
||||
public const char AdminChatAlias = ']';
|
||||
public const char RadioAlias = ';';
|
||||
|
||||
private readonly List<StoredChatMessage> _filteredHistory = new();
|
||||
|
||||
// Filter Button States
|
||||
private bool _allState;
|
||||
private bool _localState;
|
||||
private bool _oocState;
|
||||
private bool _adminState;
|
||||
private bool _deadState;
|
||||
// currently enabled channel filters set by the user. If an entry is not in this
|
||||
// list it has not been explicitly set yet, thus will default to enabled when it first
|
||||
// becomes filterable (added to _filterableChannels)
|
||||
// Note that these are persisted here, at the manager,
|
||||
// rather than the chatbox so that these settings persist between instances of different
|
||||
// chatboxes.
|
||||
public readonly Dictionary<ChatChannel, bool> _channelFilters = new();
|
||||
|
||||
// Maintains which channels a client should be able to filter (for showing in the chatbox)
|
||||
// and select (for attempting to send on).
|
||||
// This may not always actually match with what the server will actually allow them to
|
||||
// send / receive on, it is only what the user can select in the UI. For example,
|
||||
// if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
|
||||
// to the server to handle invalid attempts to use particular channels and not send messages for
|
||||
// channels the user shouldn't be able to hear.
|
||||
//
|
||||
// Note that Command is an available selection in the chatbox channel selector,
|
||||
// which is not actually a chat channel but is always available.
|
||||
private readonly HashSet<ChatChannel> _filterableChannels = new();
|
||||
private readonly List<ChatChannel> _selectableChannels = new();
|
||||
|
||||
// Flag Enums for holding filtered channels
|
||||
private ChatChannel _filteredChannels;
|
||||
|
||||
/// <summary>
|
||||
/// For currently disabled chat filters,
|
||||
/// unread messages (messages received since the channel has been filtered
|
||||
/// out). Never goes above 10 (9+ should be shown when at 10)
|
||||
/// </summary>
|
||||
private readonly Dictionary<ChatChannel, byte> _unreadMessages = new();
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IClientConGroupController _groupController = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||
|
||||
private ChatBox? _currentChatBox;
|
||||
/// <summary>
|
||||
/// Current chat box control. This can be modified, so do not depend on saving a reference to this.
|
||||
/// </summary>
|
||||
public ChatBox? CurrentChatBox { get; private set; }
|
||||
/// <summary>
|
||||
/// Invoked when CurrentChatBox is resized (including after setting initial default size)
|
||||
/// </summary>
|
||||
public event Action<ChatResizedEventArgs>? OnChatBoxResized;
|
||||
|
||||
private Control _speechBubbleRoot = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -110,6 +139,123 @@ namespace Content.Client.Chat
|
||||
_netManager.Connected += RequestMaxLength;
|
||||
}
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_adminMgr.AdminStatusUpdated += UpdateChannelPermissions;
|
||||
_playerManager.LocalPlayerChanged += OnLocalPlayerChanged;
|
||||
OnLocalPlayerChanged(new LocalPlayerChangedEventArgs(null, _playerManager.LocalPlayer));
|
||||
}
|
||||
|
||||
private void OnLocalPlayerChanged(LocalPlayerChangedEventArgs obj)
|
||||
{
|
||||
if (obj.OldPlayer != null)
|
||||
{
|
||||
obj.OldPlayer.EntityAttached -= OnLocalPlayerEntityAttached;
|
||||
obj.OldPlayer.EntityDetached -= OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
if (obj.NewPlayer != null)
|
||||
{
|
||||
obj.NewPlayer.EntityAttached += OnLocalPlayerEntityAttached;
|
||||
obj.NewPlayer.EntityDetached += OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityAttached(EntityAttachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityDetached(EntityDetachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
// go through all of the various channels and update filter / select permissions
|
||||
// appropriately, also enabling them if our enabledChannels dict doesn't have an entry
|
||||
// for any newly-granted channels
|
||||
private void UpdateChannelPermissions()
|
||||
{
|
||||
// can always send/recieve OOC
|
||||
if (!_selectableChannels.Contains(ChatChannel.OOC))
|
||||
{
|
||||
_selectableChannels.Add(ChatChannel.OOC);
|
||||
}
|
||||
AddFilterableChannel(ChatChannel.OOC);
|
||||
|
||||
// can always hear server (nobody can actually send server messages).
|
||||
AddFilterableChannel(ChatChannel.Server);
|
||||
|
||||
// can always hear local / radio / emote
|
||||
AddFilterableChannel(ChatChannel.Local);
|
||||
AddFilterableChannel(ChatChannel.Radio);
|
||||
AddFilterableChannel(ChatChannel.Emotes);
|
||||
|
||||
// Can only send local / radio / emote when attached to a non-ghost entity.
|
||||
// TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
|
||||
if (!_playerManager.LocalPlayer?.ControlledEntity?.HasComponent<GhostComponent>() ?? false)
|
||||
{
|
||||
_selectableChannels.Add(ChatChannel.Local);
|
||||
_selectableChannels.Add(ChatChannel.Radio);
|
||||
_selectableChannels.Add(ChatChannel.Emotes);
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectableChannels.Remove(ChatChannel.Local);
|
||||
_selectableChannels.Remove(ChatChannel.Radio);
|
||||
_selectableChannels.Remove(ChatChannel.Emotes);
|
||||
}
|
||||
|
||||
// Only ghosts and admins can send / see deadchat.
|
||||
// TODO: Should spectators also be able to see deadchat?
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin) ||
|
||||
(_playerManager?.LocalPlayer?.ControlledEntity?.HasComponent<GhostComponent>() ?? false))
|
||||
{
|
||||
AddFilterableChannel(ChatChannel.Dead);
|
||||
if (!_selectableChannels.Contains(ChatChannel.Dead))
|
||||
{
|
||||
_selectableChannels.Add(ChatChannel.Dead);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_filterableChannels.Remove(ChatChannel.Dead);
|
||||
_selectableChannels.Remove(ChatChannel.Dead);
|
||||
}
|
||||
|
||||
// only admins can see / filter asay
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
{
|
||||
AddFilterableChannel(ChatChannel.AdminChat);
|
||||
if (!_selectableChannels.Contains(ChatChannel.AdminChat))
|
||||
{
|
||||
_selectableChannels.Add(ChatChannel.AdminChat);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectableChannels.Remove(ChatChannel.AdminChat);
|
||||
_filterableChannels.Remove(ChatChannel.AdminChat);
|
||||
}
|
||||
|
||||
// let our chatbox know all the new settings
|
||||
CurrentChatBox?.SetChannelPermissions(_selectableChannels, _filterableChannels, _channelFilters, _unreadMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the channel to the set of filterable channels, defaulting it as enabled
|
||||
/// if it doesn't currently have an explicit enable/disable setting
|
||||
/// </summary>
|
||||
private void AddFilterableChannel(ChatChannel channel)
|
||||
{
|
||||
if (!_channelFilters.ContainsKey(channel))
|
||||
_channelFilters[channel] = true;
|
||||
_filterableChannels.Add(channel);
|
||||
}
|
||||
|
||||
|
||||
public void FrameUpdate(FrameEventArgs delta)
|
||||
{
|
||||
// Update queued speech bubbles.
|
||||
@@ -150,29 +296,31 @@ namespace Content.Client.Chat
|
||||
|
||||
public void SetChatBox(ChatBox chatBox)
|
||||
{
|
||||
if (_currentChatBox != null)
|
||||
if (CurrentChatBox != null)
|
||||
{
|
||||
_currentChatBox.TextSubmitted -= OnChatBoxTextSubmitted;
|
||||
_currentChatBox.FilterToggled -= OnFilterButtonToggled;
|
||||
CurrentChatBox.TextSubmitted -= OnChatBoxTextSubmitted;
|
||||
CurrentChatBox.FilterToggled -= OnFilterButtonToggled;
|
||||
CurrentChatBox.OnResized -= ChatBoxOnResized;
|
||||
}
|
||||
|
||||
_currentChatBox = chatBox;
|
||||
if (_currentChatBox != null)
|
||||
CurrentChatBox = chatBox;
|
||||
if (CurrentChatBox != null)
|
||||
{
|
||||
_currentChatBox.TextSubmitted += OnChatBoxTextSubmitted;
|
||||
_currentChatBox.FilterToggled += OnFilterButtonToggled;
|
||||
CurrentChatBox.TextSubmitted += OnChatBoxTextSubmitted;
|
||||
CurrentChatBox.FilterToggled += OnFilterButtonToggled;
|
||||
CurrentChatBox.OnResized += ChatBoxOnResized;
|
||||
|
||||
_currentChatBox.AllButton.Pressed = !_allState;
|
||||
_currentChatBox.LocalButton.Pressed = !_localState;
|
||||
_currentChatBox.OOCButton.Pressed = !_oocState;
|
||||
_currentChatBox.AdminButton.Pressed = !_adminState;
|
||||
_currentChatBox.DeadButton.Pressed = !_deadState;
|
||||
AdminStatusUpdated();
|
||||
CurrentChatBox.SetChannelPermissions(_selectableChannels, _filterableChannels, _channelFilters, _unreadMessages);
|
||||
}
|
||||
|
||||
RepopulateChat(_filteredHistory);
|
||||
}
|
||||
|
||||
private void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs)
|
||||
{
|
||||
OnChatBoxResized?.Invoke(chatResizedEventArgs);
|
||||
}
|
||||
|
||||
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
||||
{
|
||||
bubble.Dispose();
|
||||
@@ -193,6 +341,15 @@ namespace Content.Client.Chat
|
||||
if (IsFiltered(message.Channel))
|
||||
{
|
||||
Logger.Debug($"Message filtered: {message.Channel}: {message.Message}");
|
||||
// accumulate unread
|
||||
if (message.Read) return;
|
||||
if (!_unreadMessages.TryGetValue(message.Channel, out var count))
|
||||
{
|
||||
count = 0;
|
||||
}
|
||||
count = (byte) Math.Min(count + 1, 10);
|
||||
_unreadMessages[message.Channel] = count;
|
||||
CurrentChatBox?.UpdateUnreadMessageCounts(_unreadMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,12 +377,15 @@ namespace Content.Client.Chat
|
||||
};
|
||||
}
|
||||
|
||||
_currentChatBox?.AddLine(FormattedMessage.FromMarkup(messageText), color);
|
||||
if (CurrentChatBox == null) return;
|
||||
CurrentChatBox.AddLine(messageText, message.Channel, color);
|
||||
// TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to
|
||||
message.Read = true;
|
||||
}
|
||||
|
||||
private void OnChatBoxTextSubmitted(ChatBox chatBox, string text)
|
||||
{
|
||||
DebugTools.Assert(chatBox == _currentChatBox);
|
||||
DebugTools.Assert(chatBox == CurrentChatBox);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
@@ -233,12 +393,12 @@ namespace Content.Client.Chat
|
||||
// Check if message is longer than the character limit
|
||||
if (text.Length > _maxMessageLength)
|
||||
{
|
||||
if (_currentChatBox != null)
|
||||
if (CurrentChatBox != null)
|
||||
{
|
||||
string locWarning = Loc.GetString("chat-manager-max-message-length",
|
||||
("maxMessageLength", _maxMessageLength));
|
||||
_currentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
||||
_currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
|
||||
CurrentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
||||
CurrentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -265,7 +425,7 @@ namespace Content.Client.Chat
|
||||
var conInput = text.Substring(1);
|
||||
if (string.IsNullOrWhiteSpace(conInput))
|
||||
return;
|
||||
if (_groupController.CanCommand("asay"))
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
{
|
||||
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(conInput)}\"");
|
||||
}
|
||||
@@ -286,8 +446,8 @@ namespace Content.Client.Chat
|
||||
}
|
||||
default:
|
||||
{
|
||||
var conInput = _currentChatBox?.DefaultChatFormat != null
|
||||
? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text))
|
||||
var conInput = CurrentChatBox?.DefaultChatFormat != null
|
||||
? string.Format(CurrentChatBox.DefaultChatFormat, CommandParsing.Escape(text))
|
||||
: text;
|
||||
_consoleHost.ExecuteCommand(conInput);
|
||||
break;
|
||||
@@ -295,63 +455,19 @@ namespace Content.Client.Chat
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterButtonToggled(ChatBox chatBox, BaseButton.ButtonToggledEventArgs e)
|
||||
private void OnFilterButtonToggled(ChatChannel channel, bool enabled)
|
||||
{
|
||||
switch (e.Button.Name)
|
||||
if (enabled)
|
||||
{
|
||||
case "Local":
|
||||
_localState = !_localState;
|
||||
if (_localState)
|
||||
{
|
||||
_filteredChannels |= ChatChannel.Local;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_filteredChannels &= ~ChatChannel.Local;
|
||||
break;
|
||||
}
|
||||
|
||||
case "OOC":
|
||||
_oocState = !_oocState;
|
||||
if (_oocState)
|
||||
{
|
||||
_filteredChannels |= ChatChannel.OOC;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_filteredChannels &= ~ChatChannel.OOC;
|
||||
break;
|
||||
}
|
||||
case "Admin":
|
||||
_adminState = !_adminState;
|
||||
if (_adminState)
|
||||
{
|
||||
_filteredChannels |= ChatChannel.AdminChat;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_filteredChannels &= ~ChatChannel.AdminChat;
|
||||
break;
|
||||
}
|
||||
case "Dead":
|
||||
_deadState = !_deadState;
|
||||
if (_deadState)
|
||||
_filteredChannels |= ChatChannel.Dead;
|
||||
else
|
||||
_filteredChannels &= ~ChatChannel.Dead;
|
||||
break;
|
||||
|
||||
case "ALL":
|
||||
chatBox.LocalButton.Pressed ^= true;
|
||||
chatBox.OOCButton.Pressed ^= true;
|
||||
if (chatBox.AdminButton != null)
|
||||
chatBox.AdminButton.Pressed ^= true;
|
||||
chatBox.DeadButton.Pressed ^= true;
|
||||
_allState = !_allState;
|
||||
break;
|
||||
_channelFilters[channel] = true;
|
||||
_filteredChannels &= ~channel;
|
||||
_unreadMessages.Remove(channel);
|
||||
CurrentChatBox?.UpdateUnreadMessageCounts(_unreadMessages);
|
||||
}
|
||||
else
|
||||
{
|
||||
_channelFilters[channel] = false;
|
||||
_filteredChannels |= channel;
|
||||
}
|
||||
|
||||
RepopulateChat(_filteredHistory);
|
||||
@@ -359,12 +475,12 @@ namespace Content.Client.Chat
|
||||
|
||||
private void RepopulateChat(IEnumerable<StoredChatMessage> filteredMessages)
|
||||
{
|
||||
if (_currentChatBox == null)
|
||||
if (CurrentChatBox == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentChatBox.Contents.Clear();
|
||||
CurrentChatBox.Contents.Clear();
|
||||
|
||||
foreach (var msg in filteredMessages)
|
||||
{
|
||||
@@ -522,33 +638,7 @@ namespace Content.Client.Chat
|
||||
|
||||
private bool IsFiltered(ChatChannel channel)
|
||||
{
|
||||
// _allState works as inverter.
|
||||
return _allState ^ _filteredChannels.HasFlag(channel);
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_adminMgr.AdminStatusUpdated += AdminStatusUpdated;
|
||||
}
|
||||
|
||||
private void AdminStatusUpdated()
|
||||
{
|
||||
if (_currentChatBox != null)
|
||||
{
|
||||
_currentChatBox.AdminButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin);
|
||||
_currentChatBox.DeadButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin);
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleDeadChatButtonVisibility(bool visibility)
|
||||
{
|
||||
if (_currentChatBox != null)
|
||||
{
|
||||
// If the user is an admin and returned to body, don't set the flag as null
|
||||
if (!visibility && _adminMgr.HasFlag(AdminFlags.Admin))
|
||||
return;
|
||||
_currentChatBox.DeadButton.Visible = visibility;
|
||||
}
|
||||
return _filteredChannels.HasFlag(channel);
|
||||
}
|
||||
|
||||
private sealed class SpeechBubbleQueueData
|
||||
|
||||
Reference in New Issue
Block a user