Files
HellionChat/HellionChat/Ui/Components/Sidebar.cs
T
JonKazama-Hellion eca566321a fix(tells): a tell no longer takes the keyboard mid-sentence
Reported by Carla: a tell arriving while you are typing pulls the focus
away. The interruption is the visible half. The sharp half is that the
input buffer belongs to the WINDOW while the send target is read off
whatever tab is active at Enter -- so a line typed at one person could
leave addressed to whoever just wrote, and in this game losing the
keyboard means the next sentence walks the character around.

Nothing is revealed now while any chat surface is mid-sentence, in any
mode. The tab still appears and still carries its unread mark. The check
lives in its own file because the answer has to be identical everywhere:
it started inside the reveal plan, and a second pop-out path walked
straight past it -- AutoTellTabsService opened windows off its own flag,
at tab creation, a tick before the router was ever asked.

Those two paths are one now. AutoTellTabsOpenAsPopout and TellAutoOpenMode
were two settings for one decision, and the older one won every race,
which is why the other looked inert. Config schema 28 carries the old flag
forward so nobody's behaviour changes. "Off" went with it: it never
stopped the tab from being created -- that is the auto-tell switch -- it
only stopped the jump to it, which is what the switch below it does.

Also in here, all from the same corner of the code:

- Closing a tab was lost in the v2.0.0 rebuild. The trash entry lived in
  the retired ChatLogWindow menu, and the rebuilt one restored rename,
  sound, pop-out and pinning but not this. For tell tabs that left no way
  out at all: IsEditable keeps them out of the settings editor on purpose
  and points at the context menu, which could not close them either.
  Pinned tell tabs stay disabled with a tooltip rather than absent.
- Re-anchoring the active tab used an unconditional Tabs[0] in three
  places, and Tabs[0] can be popped out -- so it ran OnTabActivated over a
  tab live in its own window and stripped its tell binding. With every tab
  popped, the seed and the re-anchor also fought each other every frame.
- PinTab_LimitReached still pointed at "Promote to permanent", removed in
  May. Spanish said "Desija", which is not a word; Greek left "tell tabs"
  untranslated; pt-PT broke its own unpin verb.
- Pop Out was a hardcoded English literal despite the key existing in all
  25 languages since v1.5.6, and the tell-open modes were the last English
  display names in the plugin.
- Segmented setting rows measured 200px flat, which cut German labels in
  half. They size to their longest label now.
- Metrics.Scale still called GlobalScaleSafe. It is an alias for
  GlobalScale in current Dalamud, and dropping it clears the last compiler
  warning in the project.
2026-08-23 02:12:27 +02:00

605 lines
25 KiB
C#

using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Ui.StyleEngine.Widgets;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Components;
// Channel-list panel pinned to the left of the chat window. Auto-switches
// between an icon-only column (38px) and an expanded column (Config.SidebarWidth) once
// the outer window crosses Config.SidebarAutoSwitchThresholdPx. The
// pop-out affordance (hover button + right-click menu) routes through the
// injected ChannelPopoutPool via TryOpen, which reserves a slot and binds
// the tab to a pre-allocated pop-out window.
internal sealed class Sidebar
{
public const float IconOnlyWidth = 38f;
// Expanded sidebar width is user-configurable (Config.SidebarWidth),
// clamped to these bounds (matches the ChannelsTab slider range).
//
// The floor is where a tab name stops being readable, not where the icons
// stop fitting: 40 let the expanded sidebar be narrower than the icon-only
// one, which drew labels into a column too narrow to hold them. Anyone who
// wants it that slim wants the collapsed layout, and that is IconOnlyWidth.
public const float MinSidebarWidth = 130f;
public const float MaxSidebarWidth = 300f;
private static float RowHeight => Metrics.SidebarRowHeight;
private static float PopOutHitWidth => Metrics.SidebarPopOutHitWidth;
private static float GreetedHitWidth => Metrics.SidebarGreetedHitWidth;
// Counts rows that got the active surface this frame. At most one, with two
// exceptions: zero when every tab is popped out (PickMainActiveTab returns
// null), and two in the frame a click lands on a row drawn after the
// previously active one -- that row was still active when it was painted.
internal int LastRenderedActiveSurfaceCount { get; private set; }
// render observability: counts greeted glyphs actually drawn this frame.
// Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start.
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
internal int LastRenderedGreetedGlyphCount;
internal int LastRenderedUnreadDotCount;
// Small enough to read as a marker on the icon rather than as a second icon
// beside it.
private const float PinGlyphScale = 0.6f;
// render observability: section headers actually drawn this frame.
// Incremented only in the real header branch; reset at Draw start.
internal int LastDrawnSectionHeaderCount;
// Inline mirror of the old TabIconMapping table so the Ui layer carries
// its own glyph lookup once the standalone file is removed.
private static readonly Dictionary<string, FontAwesomeIcon> IconByName = new(
StringComparer.OrdinalIgnoreCase
)
{
["comment"] = FontAwesomeIcon.Comment,
["comments"] = FontAwesomeIcon.Comments,
["cog"] = FontAwesomeIcon.Cog,
["users"] = FontAwesomeIcon.Users,
["user-friends"] = FontAwesomeIcon.UserFriends,
["link"] = FontAwesomeIcon.Link,
["envelope"] = FontAwesomeIcon.Envelope,
["clock"] = FontAwesomeIcon.Clock,
["hashtag"] = FontAwesomeIcon.Hashtag,
["star"] = FontAwesomeIcon.Star,
["heart"] = FontAwesomeIcon.Heart,
["bell"] = FontAwesomeIcon.Bell,
["bookmark"] = FontAwesomeIcon.Bookmark,
["flag"] = FontAwesomeIcon.Flag,
["fire"] = FontAwesomeIcon.Fire,
};
private readonly ThemeRegistry _themes;
private readonly TokenResolver _resolver;
private readonly WidgetPalette _palette;
private readonly FontManager _fonts;
private readonly ILogger<Sidebar> _logger;
private readonly Windows.ChannelPopoutPool _pool;
public Sidebar(
ThemeRegistry themes,
TokenResolver resolver,
FontManager fonts,
ILogger<Sidebar> logger,
Windows.ChannelPopoutPool pool
)
{
_themes = themes;
_resolver = resolver;
_palette = new WidgetPalette(resolver);
_fonts = fonts;
_logger = logger;
_pool = pool;
}
// Both stay unscaled. The stored width and the switch threshold are user
// settings in design pixels, and SidebarModeAutoSwitchStep compares this
// return value against the raw bounds. Display scaling is applied once, at
// the single draw call site below.
public bool IsExpanded(float windowWidth) =>
windowWidth >= Plugin.Config.SidebarAutoSwitchThresholdPx;
public float GetWidth(float windowWidth) =>
IsExpanded(windowWidth)
? Math.Clamp((float)Plugin.Config.SidebarWidth, MinSidebarWidth, MaxSidebarWidth)
: IconOnlyWidth;
// Factored click logic so the SelfTest exercises the real toggle, not a direct
// MarkGreeted call (which would be a dead path the render never takes).
internal void ToggleGreetedForSelfTest(Tab tab)
{
if (Plugin.Instance.AutoTellTabsService.IsGreeted(tab))
Plugin.Instance.AutoTellTabsService.UnmarkGreeted(tab);
else
Plugin.Instance.AutoTellTabsService.MarkGreeted(tab);
}
public void Draw(float windowWidth, IReadOnlyList<Tab> tabs, ref Tab? activeTab)
{
LastRenderedGreetedGlyphCount = 0;
LastRenderedUnreadDotCount = 0;
LastDrawnSectionHeaderCount = 0;
LastRenderedActiveSurfaceCount = 0;
if (!_fonts.FontsReady)
{
// A scale change triggers a font rebuild, so this branch is really
// hit while GlobalScale is moving -- an unscaled width here makes
// the sidebar jump.
ImGui.Dummy(new Vector2(Metrics.SidebarIconOnlyWidth, 0));
return;
}
var expanded = IsExpanded(windowWidth);
var width = GetWidth(windowWidth) * Metrics.Scale;
using var child = ImRaii.Child("##hellion-sidebar", new Vector2(width, 0));
if (!child.Success)
return;
var theme = _themes.Active;
var accentRgba = _resolver.Resolve(Token.AccentPrimary, theme.Colors);
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
var dl = ImGui.GetWindowDrawList();
// sectioned render order (1.5.6 parity): persistent → pinned
// TempTabs → unpinned TempTabs. Only the display sequence regroups;
// the tab list itself stays untouched and every row keeps its
// ORIGINAL list index for PushID, so an open context-menu popup
// stays bound to its tab when sectioning moves it visually.
var renderOrder = TabLifecycleHelpers.BuildRenderOrder(
tabs,
t => _pool.IsOpen(t.Identifier)
);
var pinnedHeaderRendered = false;
var unpinnedHeaderRendered = false;
// Rows carry their own full-height surface now, so the default gap
// between them would read as a stripe of window background. The section
// headers are unaffected: LineDivider brings its own padding.
using var rowSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
foreach (var i in renderOrder)
{
var tab = tabs[i];
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
{
DrawSectionHeader(
HellionStrings.PinTab_SectionHeader,
TabLifecycleHelpers.CountPinnedPool(tabs, t => _pool.IsOpen(t.Identifier))
);
pinnedHeaderRendered = true;
}
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !unpinnedHeaderRendered)
{
DrawSectionHeader(
HellionStrings.AutoTellTabs_SectionHeader,
TabLifecycleHelpers.CountUnpinnedPool(tabs, t => _pool.IsOpen(t.Identifier))
);
unpinnedHeaderRendered = true;
}
DrawRow(
tab,
tabs,
expanded,
accentRgba,
textAbgr,
mutedAbgr,
dimAbgr,
dl,
ref activeTab
);
}
}
// Section transition marker (1.5.6 parity): the rule always renders, compact
// mode suppresses only the caption. LineDivider submits its own layout item
// and carries its own padding, which is what lets the rows below sit flush
// without the header collapsing onto them.
private void DrawSectionHeader(string header, int count)
{
var colors = _themes.Active.Colors;
var compact = Plugin.Config.AutoTellTabsCompactDisplay;
LineDivider.Draw(
compact ? null : $"{header} ({count})",
_palette.Abgr(Token.Border, colors),
_palette.Abgr(Token.TextMuted, colors)
);
if (!compact)
LastDrawnSectionHeaderCount++;
}
private void DrawRow(
Tab tab,
IReadOnlyList<Tab> tabs,
bool expanded,
uint accentRgba,
uint textAbgr,
uint mutedAbgr,
uint dimAbgr,
ImDrawListPtr dl,
ref Tab? activeTab
)
{
// Identity, not position: ImGui keeps popup state across frames under this
// ID, so an index would re-bind an open context menu to a different tab as
// soon as the list shifts. String, not GetHashCode — hashes collide.
ImGui.PushID(tab.Identifier.ToString());
var origin = ImGui.GetCursorScreenPos();
var avail = ImGui.GetContentRegionAvail().X;
// Drop the row entirely when the sidebar is dragged below the width
// of a single hit target. ImGui's InvisibleButton asserts on a
// zero-width size, which crashes the whole window at min-drag.
if (avail < Metrics.SidebarMinDrawWidth)
{
ImGui.PopID();
return;
}
// 1.5.6 parity: greeted state dims the tab icon whenever the toggle is
// configured on. The clickable affordance additionally needs an expanded
// sidebar with room for a third hit area beside the pop-out slot — in
// icon-only or min-drag mode it is skipped entirely.
var greetedConfigured = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
var showGreeted =
greetedConfigured
&& expanded
&& avail > GreetedHitWidth + PopOutHitWidth + Metrics.SidebarHitSlack;
// Only split off a separate pop-out hit area when there's room for
// both buttons. Below that, the whole row stays as a single
// selectable strip without the pop-out affordance.
// Gate the pop-out affordance on the expanded sidebar too. In
// icon-only mode avail still clears the width threshold, which used to
// paint the pop-out glyph over the tab icon. The row stays a single
// selectable strip when collapsed; right-click pop-out is unaffected.
var hasPopOut = expanded && avail > PopOutHitWidth + Metrics.SidebarHitSlack;
var tabHitWidth = hasPopOut ? avail - PopOutHitWidth : avail;
if (showGreeted)
{
// Greeted slot sits at the left edge (1.5.6 placement); the row
// button starts after it so the three hit areas never overlap.
tabHitWidth -= GreetedHitWidth;
ImGui.SetCursorScreenPos(origin + new Vector2(GreetedHitWidth, 0f));
}
ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight));
if (ImGui.IsItemClicked())
{
var previous = activeTab;
activeTab = tab;
TabLifecycleHelpers.OnTabActivated(tab, previous);
}
// Not IsItemHovered: the row button is up to two hit widths narrower than
// the row, so a full-width surface driven by it would flicker at the
// edges. AllowWhenBlockedByActiveItem keeps the surface while the button
// is held down; without it the fill vanishes on press.
var rowMax = origin + new Vector2(avail, RowHeight);
var surfaceHovered =
ImGui.IsMouseHoveringRect(origin, rowMax)
&& ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
// GetID is seeded from the window's ID stack, so the same "row" literal
// stays distinct per window and per PushID'd tab. The old interpolated
// key allocated two strings per row per frame.
var hoverId = ImGui.GetID("row"u8);
var hoverAmount = HoverState.Query(hoverId, surfaceHovered);
var isActiveRow = ReferenceEquals(tab, activeTab);
if (isActiveRow)
LastRenderedActiveSurfaceCount++;
var colors = _themes.Active.Colors;
// Row fills follow the window's own opacity. Theme surfaces are fully
// opaque and the window is translucent by default (0.85 focused, 0.65
// not), so unscaled fills would sit on top as solid blocks.
var opacity = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootWindow)
? Plugin.Config.WindowOpacity
: Plugin.Config.WindowOpacityInactive;
Row.Draw(
origin,
new Vector2(avail, RowHeight),
new RowVisualState
{
IsActive = isActiveRow,
HoverAmount = hoverAmount,
SurfaceHoverAbgr = ColourUtil.ApplyAlpha(
_palette.Abgr(Token.SurfaceHover, colors),
opacity
),
SurfaceActiveAbgr = ColourUtil.ApplyAlpha(
_palette.Abgr(Token.SurfaceActive, colors),
opacity
),
AccentAbgr = _palette.Abgr(Token.AccentPrimary, colors),
BorderAbgr = ColourUtil.ApplyAlpha(_palette.Abgr(Token.Border, colors), opacity),
},
new RowStyle
{
// Across the gap between the sidebar group and the message
// area, so the selected tab touches the conversation it
// selects instead of ending at the seam.
ActiveBridgeWidth = ImGui.GetStyle().ItemSpacing.X,
}
);
dl.DrawHoverSheen(origin, rowMax, accentRgba, hoverAmount, surfaceHovered);
var icon = ResolveTabIcon(tab);
// Dim precedence (1.5.6): the active tab never dims; only greeted,
// non-active tabs drop to TextDim. "Regular colour" is the tint for an
// auto-tell tab and the theme text colour for everything else.
//
// Below that, an auto-tell tab is tinted from its partner. Twelve
// colours against seven glyphs is 84 combinations, which is plenty for
// the one to five conversations anybody actually runs in parallel. The
// greeted dim still wins: it says something about this tab right now,
// the tint only says who it belongs to.
var isCurrentTab = tab == activeTab;
var iconColor = tab.IsTempTab ? ColourUtil.RgbaToAbgr(TabTintCache.GetTint(tab)) : textAbgr;
if (
!isCurrentTab
&& greetedConfigured
&& Plugin.Instance.AutoTellTabsService.IsGreeted(tab)
)
iconColor = dimAbgr;
// Icon and label shift right by the greeted slot when it is shown.
var contentX = showGreeted ? GreetedHitWidth : 0f;
var scale = Metrics.Scale;
var iconInset = 10f * scale;
// Centred, not a frozen offset: the old 8f was (32 - 16) / 2 for a 16px
// font and stays wrong at any other Config.FontSizeV2.
var contentY = Metrics.CenterY(RowHeight);
float iconRight;
using (_fonts.FontAwesome.Push())
{
// Measured inside the push: FontAwesome is a fixed-width icon handle
// that does not follow Config.FontSizeV2, so the text font's line
// height would misplace the glyph at any other body size.
var iconStr = icon.ToIconString();
var iconSize = ImGui.CalcTextSize(iconStr);
var iconPos =
origin
+ new Vector2(iconInset + contentX, MetricsMath.CenterY(RowHeight, iconSize.Y));
dl.AddText(iconPos, iconColor, iconStr);
iconRight = iconInset + contentX + iconSize.X;
// Pinned marker: a small thumbtack tucked into the icon's lower
// left. Drawn from the same font push and inside the row rectangle
// the InvisibleButton already reserved, so it claims no layout of
// its own -- badges in this very sidebar are where "draw into
// unreserved space" caught this project last.
//
// Lower left because the unread dot owns the upper right.
if (tab.IsPinned)
{
var pinStr = FontAwesomeIcon.Thumbtack.ToIconString();
var pinFontSize = ImGui.GetFontSize() * PinGlyphScale;
var pinSize = ImGui.CalcTextSize(pinStr) * PinGlyphScale;
dl.AddText(
ImGui.GetFont(),
pinFontSize,
iconPos + new Vector2(-pinSize.X * 0.45f, iconSize.Y - pinSize.Y * 0.75f),
_palette.Abgr(Token.AccentPrimary, colors),
pinStr
);
}
}
// Only for pinned rows, and only in the sidebar's own hover state --
// this is the one place a user meets the marker without having opened
// the menu that produced it.
if (tab.IsPinned && surfaceHovered)
ImGuiUtil.Tooltip(HellionStrings.PinTab_PinnedTooltip);
if (expanded)
dl.AddText(
origin + new Vector2(iconRight + 6f * scale, contentY),
textAbgr,
TabDisplayName.Resolve(
tab.Name,
tab.NameCameFromPartner,
Plugin.Config.ScreenshotMode
)
);
// Unread count. Drawn outside the icon-font scope on purpose: the
// FontAwesome atlas carries no ASCII digits, so the number would come out
// blank. The active tab is zeroed every frame (MainWindow.Draw), so it
// never shows on the tab you are viewing; UnreadMode.None opts out.
if (!isCurrentTab && tab.UnreadMode != UnreadMode.None && tab.Unread > 0)
{
var unread = (int)Math.Min(tab.Unread, int.MaxValue);
var badgeSize = Badge.CalcSize(unread);
var accentAbgr = _palette.Abgr(Token.AccentEmber, colors);
var slack = Metrics.SidebarHitSlack;
var reserved = hasPopOut ? PopOutHitWidth : 0f;
var badgeX = avail - reserved - badgeSize.X - slack;
// The count only fits where it can sit clear of the icon. At the
// default sidebar width of 44 it cannot, so a plain dot takes over
// rather than the badge landing on the glyph.
if (badgeX >= iconRight + slack)
{
Badge.Draw(
origin + new Vector2(badgeX, MetricsMath.CenterY(RowHeight, badgeSize.Y)),
unread,
accentAbgr,
textAbgr
);
}
else
{
var r = Metrics.SidebarUnreadRadius;
dl.AddCircleFilled(
origin + new Vector2(iconRight - r * 0.5f, RowHeight * 0.5f - r),
r,
accentAbgr,
12
);
}
LastRenderedUnreadDotCount++;
}
TabContextMenu.Draw(tab, "ctx", _pool, tabs);
if (hasPopOut)
{
ImGui.SameLine(0f, 0f);
// Glyph follows the row surface, not the button's own hover: the
// button sits inside the row, and the old pairing needed a hover
// state one line before it existed.
var (popClicked, _) = IconButton.Draw(
ImGui.GetID("popout"u8),
new Vector2(PopOutHitWidth, RowHeight),
surfaceHovered ? FontAwesomeIcon.ArrowUpRightFromSquare : null,
mutedAbgr,
_palette.Abgr(Token.SurfaceHover, colors),
_fonts.FontAwesome
);
if (popClicked)
_pool.TryOpen(tab);
}
if (showGreeted)
{
// The hit area sits at the LEFT edge of the row, but the item must
// be submitted AFTER TabContextMenu.Draw — any interactive item
// between the row button and the popup call would steal the
// right-click trigger (ordering constraint).
ImGui.SetCursorScreenPos(origin);
// CheckCircle = greeted, plain Check = still pending (1.5.6 mapping).
var greetedGlyph = Plugin.Instance.AutoTellTabsService.IsGreeted(tab)
? FontAwesomeIcon.CheckCircle
: FontAwesomeIcon.Check;
var (greetedClicked, _) = IconButton.Draw(
ImGui.GetID("greeted"u8),
new Vector2(GreetedHitWidth, RowHeight),
greetedGlyph,
mutedAbgr,
_palette.Abgr(Token.SurfaceHover, colors),
_fonts.FontAwesome
);
if (greetedClicked)
ToggleGreetedForSelfTest(tab);
LastRenderedGreetedGlyphCount++;
}
ImGui.PopID();
}
// internal since v1.13.0: the channel header shows the same icon as the row
// in here, and it has to resolve it the same way. IconByName alone would not
// do -- it only answers for a tab with an explicitly chosen icon, and the
// default is none, so most tabs fall through to the derivation below.
internal static FontAwesomeIcon ResolveTabIcon(Tab tab)
{
if (
!string.IsNullOrWhiteSpace(tab.Icon) && IconByName.TryGetValue(tab.Icon, out var mapped)
)
return mapped;
// Auto-tell tabs get one of seven glyphs derived from the partner, not
// one envelope for all of them. With four tells open, identical rows in
// identical colour are four rows you have to read to tell apart.
if (tab.IsTempTab)
{
// TryGetValue rather than an indexer: every glyph in the pool is in
// the table today, and a lookup miss would be somebody editing one
// of the two lists without the other. A wrong envelope beats a
// KeyNotFoundException on the draw thread.
var hashed = TabTintCache.GetIcon(tab);
return IconByName.TryGetValue(hashed, out var tellGlyph)
? tellGlyph
: FontAwesomeIcon.Envelope;
}
// Channel-type fallback. Walk every selected key, not just the first,
// so a System tab that filters multiple system-flavoured ChatTypes
// still picks up fa-cog when one of the later keys carries the match.
// The Comment default only wins when every key falls into the
// generic-text bucket (Say / Yell / Shout etc.).
foreach (var chatType in tab.SelectedChannels.Keys)
{
var glyph = ResolveByChannelType(chatType);
if (glyph != FontAwesomeIcon.Comment)
return glyph;
}
// Last-resort name match for tabs that filter exotic ChatTypes the
// mapping above doesn't cover — keeps the System tab visually
// distinct even with a custom channel set.
if (tab.Name.Contains("system", StringComparison.OrdinalIgnoreCase))
return FontAwesomeIcon.Cog;
return FontAwesomeIcon.Comment;
}
private static FontAwesomeIcon ResolveByChannelType(ChatType type) =>
type switch
{
ChatType.TellIncoming or ChatType.TellOutgoing => FontAwesomeIcon.Envelope,
ChatType.FreeCompany
or ChatType.FreeCompanyAnnouncement
or ChatType.FreeCompanyLoginLogout => FontAwesomeIcon.Users,
ChatType.Linkshell1
or ChatType.Linkshell2
or ChatType.Linkshell3
or ChatType.Linkshell4
or ChatType.Linkshell5
or ChatType.Linkshell6
or ChatType.Linkshell7
or ChatType.Linkshell8
or ChatType.CrossLinkshell1
or ChatType.CrossLinkshell2
or ChatType.CrossLinkshell3
or ChatType.CrossLinkshell4
or ChatType.CrossLinkshell5
or ChatType.CrossLinkshell6
or ChatType.CrossLinkshell7
or ChatType.CrossLinkshell8 => FontAwesomeIcon.Link,
ChatType.Party or ChatType.CrossParty => FontAwesomeIcon.UserFriends,
ChatType.Alliance => FontAwesomeIcon.Users,
ChatType.NoviceNetwork or ChatType.NoviceNetworkSystem => FontAwesomeIcon.Users,
ChatType.PvpTeam or ChatType.PvpTeamAnnouncement or ChatType.PvpTeamLoginLogout =>
FontAwesomeIcon.Users,
ChatType.System
or ChatType.BattleSystem
or ChatType.GatheringSystem
or ChatType.Error
or ChatType.Notice
or ChatType.LootNotice
or ChatType.Echo => FontAwesomeIcon.Cog,
ChatType.CustomEmote or ChatType.StandardEmote => FontAwesomeIcon.Comments,
_ => FontAwesomeIcon.Comment,
};
}