diff --git a/ChatTwo/Http/MessageProtocol/DataStructure.cs b/ChatTwo/Http/MessageProtocol/DataStructure.cs index bd60411..14b5157 100644 --- a/ChatTwo/Http/MessageProtocol/DataStructure.cs +++ b/ChatTwo/Http/MessageProtocol/DataStructure.cs @@ -7,6 +7,11 @@ public struct SwitchChannel(string name) [JsonProperty("channel")] public string Name = name; } +public struct ChannelList(Dictionary channels) +{ + [JsonProperty("channels")] public Dictionary Channels = channels; +} + public struct Messages(MessageResponse[] set) { [JsonProperty("messages")] public MessageResponse[] Set = set; diff --git a/ChatTwo/Http/MessageProtocol/OutgoingMessage.cs b/ChatTwo/Http/MessageProtocol/OutgoingMessage.cs index 22625c5..11c5ad0 100644 --- a/ChatTwo/Http/MessageProtocol/OutgoingMessage.cs +++ b/ChatTwo/Http/MessageProtocol/OutgoingMessage.cs @@ -5,6 +5,8 @@ namespace ChatTwo.Http.MessageProtocol; public class CloseEvent() : BaseEvent("close"); +public class ChannelListEvent(ChannelList channelList) : BaseEvent("channel-list", JsonConvert.SerializeObject(channelList)); + public class SwitchChannelEvent(SwitchChannel switchChannel) : BaseEvent("switch-channel", JsonConvert.SerializeObject(switchChannel)); public class NewMessageEvent(Messages messages) : BaseEvent("new-message", JsonConvert.SerializeObject(messages)); diff --git a/ChatTwo/Http/RouteController.cs b/ChatTwo/Http/RouteController.cs index d459e6f..8d52a0b 100644 --- a/ChatTwo/Http/RouteController.cs +++ b/ChatTwo/Http/RouteController.cs @@ -165,9 +165,11 @@ public class RouteController // TODO Check if reconnect or new connection var messages = await WebserverUtil.FrameworkWrapper(Core.Processing.ReadMessageList); + var channels = await Plugin.Framework.RunOnTick(Plugin.ChatLogWindow.GetAvailableChannels); var channelName = await Plugin.Framework.RunOnTick(() => Core.Processing.ReadChannelName(Plugin.ChatLogWindow.PreviousChannel)); sse.OutboundQueue.Enqueue(new NewMessageEvent(new Messages(messages))); sse.OutboundQueue.Enqueue(new SwitchChannelEvent(new SwitchChannel(channelName))); + sse.OutboundQueue.Enqueue(new ChannelListEvent(new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value)))); await sse.HandleEventLoop(ctx); diff --git a/ChatTwo/Http/ServerCore.cs b/ChatTwo/Http/ServerCore.cs index a8f023a..04438bc 100644 --- a/ChatTwo/Http/ServerCore.cs +++ b/ChatTwo/Http/ServerCore.cs @@ -1,4 +1,5 @@ -using ChatTwo.Http.MessageProtocol; +using ChatTwo.Code; +using ChatTwo.Http.MessageProtocol; using WatsonWebserver.Core; using WatsonWebserver.Lite; using ExceptionEventArgs = WatsonWebserver.Core.ExceptionEventArgs; @@ -72,6 +73,24 @@ public class ServerCore : IAsyncDisposable Plugin.Log.Error(ex, "Sending channel switch over SSE failed."); } } + + internal void SendChannelList() + { + try + { + Plugin.Framework.RunOnTick(() => + { + var channels = Plugin.ChatLogWindow.GetAvailableChannels(); + var bundledResponse = new ChannelListEvent(new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value))); + foreach (var eventServer in EventConnections) + eventServer.OutboundQueue.Enqueue(bundledResponse); + }); + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Sending channel switch over SSE failed."); + } + } #endregion #region GeneralHandlers diff --git a/ChatTwo/Http/static/start.js b/ChatTwo/Http/static/start.js index 9183ba6..9149221 100644 --- a/ChatTwo/Http/static/start.js +++ b/ChatTwo/Http/static/start.js @@ -13,12 +13,15 @@ class SSEConnection { }); this.socket.addEventListener('new-message', (event) => { - let eventData = JSON.parse(event.data); - for (let message of eventData.messages) + for (let message of JSON.parse(event.data).messages) { addMessage(message); } }); + + this.socket.addEventListener('channel-list', (event) => { + updateChannelOptions(JSON.parse(event.data).channels) + }); } send(message) { @@ -31,14 +34,30 @@ const sse = new SSEConnection(); // channel switcher function updateChannelHint(label) { - document.getElementById('channel-hint').innerText = label; + document.getElementById('channel-hint').innerHTML = label; } + document.getElementById('channel-select').addEventListener('change', (event) => { - updateChannelHint(event.target.value); // TODO: send new channel to "backend" // ws.send(...); }); +function updateChannelOptions(channels) { + let select = document.getElementById('channel-select'); + + // clear existing channels + select.innerHTML = ''; + + for (let [ name, channel ] of Object.entries(channels)) + { + let option = document.createElement('option'); + option.text = name; + option.value = channel; + + select.appendChild(option) + } +} + // functions for handling the message list function scrollMessagesToBottom() { diff --git a/ChatTwo/Http/templates/start.html b/ChatTwo/Http/templates/start.html index 12359ba..e23a472 100644 Binary files a/ChatTwo/Http/templates/start.html and b/ChatTwo/Http/templates/start.html differ diff --git a/ChatTwo/Sheets.cs b/ChatTwo/Sheets.cs new file mode 100644 index 0000000..93e4fbd --- /dev/null +++ b/ChatTwo/Sheets.cs @@ -0,0 +1,20 @@ +using Lumina.Excel; +using Lumina.Excel.GeneratedSheets; + +namespace ChatTwo; + +public static class Sheets +{ + public static readonly ExcelSheet ItemSheet; + public static readonly ExcelSheet WorldSheet; + public static readonly ExcelSheet LogFilterSheet; + public static readonly ExcelSheet TextCommandSheet; + + static Sheets() + { + ItemSheet = Plugin.DataManager.GetExcelSheet()!; + WorldSheet = Plugin.DataManager.GetExcelSheet()!; + LogFilterSheet = Plugin.DataManager.GetExcelSheet()!; + TextCommandSheet = Plugin.DataManager.GetExcelSheet()!; + } +} \ No newline at end of file diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index 2c42d15..f3443ba 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -20,7 +20,6 @@ using Dalamud.Interface.Windowing; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; -using Lumina.Excel; using Lumina.Excel.GeneratedSheets; namespace ChatTwo.Ui; @@ -91,10 +90,6 @@ public sealed class ChatLogWindow : Window private long FrameTime; // set every frame internal long LastActivityTime = Environment.TickCount64; - private readonly ExcelSheet WorldSheet; - private readonly ExcelSheet LogFilterSheet; - private readonly ExcelSheet TextCommandSheet; - internal ChatLogWindow(Plugin plugin) : base($"{Plugin.PluginName}###chat2") { Plugin = plugin; @@ -118,10 +113,6 @@ public sealed class ChatLogWindow : Window Plugin.Commands.Register("/clearlog2", "Clear the Chat 2 chat log").Execute += ClearLog; Plugin.Commands.Register("/chat2").Execute += ToggleChat; - WorldSheet = Plugin.DataManager.GetExcelSheet()!; - LogFilterSheet = Plugin.DataManager.GetExcelSheet()!; - TextCommandSheet = Plugin.DataManager.GetExcelSheet()!; - Plugin.ClientState.Login += Login; Plugin.ClientState.Logout += Logout; @@ -302,7 +293,7 @@ public sealed class ChatLogWindow : Window AddTextCommandChannel(command, type); } - var echo = Plugin.DataManager.GetExcelSheet()?.GetRow(116); + var echo = Sheets.TextCommandSheet.GetRow(116); if (echo != null) AddTextCommandChannel(echo, ChatType.Echo); } @@ -573,39 +564,10 @@ public sealed class ChatLogWindow : Window { if (popup) { - foreach (var channel in Enum.GetValues()) - { - var name = LogFilterSheet.FirstOrDefault(row => row.LogKind == (byte) channel.ToChatType())?.Name?.RawString ?? channel.ToChatType().Name(); - if (channel.IsLinkshell()) - { - var lsName = Plugin.Functions.Chat.GetLinkshellName(channel.LinkshellIndex()); - if (string.IsNullOrWhiteSpace(lsName)) - continue; - - name += $": {lsName}"; - } - - if (channel.IsCrossLinkshell()) - { - var lsName = Plugin.Functions.Chat.GetCrossLinkshellName(channel.LinkshellIndex()); - if (string.IsNullOrWhiteSpace(lsName)) - continue; - - name += $": {lsName}"; - } - - // Check if the linkshell with this index is registered in - // the ExtraChat plugin by seeing if the command is - // registered. The command gets registered only if a - // linkshell is assigned (and even gets unassigned if the - // index changes!). - if (channel.IsExtraChatLinkshell()) - if (!Plugin.CommandManager.Commands.ContainsKey(channel.Prefix())) - continue; - + var channels = GetAvailableChannels(); + foreach (var (name, channel) in channels) if (ImGui.Selectable(name)) SetChannel(channel); - } } } @@ -753,6 +715,45 @@ public sealed class ChatLogWindow : Window GameFunctions.GameFunctions.ClickNoviceNetworkButton(); } + internal Dictionary GetAvailableChannels() + { + var channels = new Dictionary(); + foreach (var channel in Enum.GetValues()) + { + var name = Sheets.LogFilterSheet.FirstOrDefault(row => row.LogKind == (byte) channel.ToChatType())?.Name?.RawString ?? channel.ToChatType().Name(); + if (channel.IsLinkshell()) + { + var lsName = Plugin.Functions.Chat.GetLinkshellName(channel.LinkshellIndex()); + if (string.IsNullOrWhiteSpace(lsName)) + continue; + + name += $": {lsName}"; + } + + if (channel.IsCrossLinkshell()) + { + var lsName = Plugin.Functions.Chat.GetCrossLinkshellName(channel.LinkshellIndex()); + if (string.IsNullOrWhiteSpace(lsName)) + continue; + + name += $": {lsName}"; + } + + // Check if the linkshell with this index is registered in + // the ExtraChat plugin by seeing if the command is + // registered. The command gets registered only if a + // linkshell is assigned (and even gets unassigned if the + // index changes!). + if (channel.IsExtraChatLinkshell()) + if (!Plugin.CommandManager.Commands.ContainsKey(channel.Prefix())) + continue; + + channels.Add(name, channel); + } + + return channels; + } + private void DrawChannelName(Tab? activeTab) { var currentChannel = ReadChannelName(activeTab); @@ -776,7 +777,7 @@ public sealed class ChatLogWindow : Window // Note: don't use HidePlayerInString here because // abbreviation settings do not affect this. playerName = HashPlayer(TellTarget.Name, TellTarget.World); - var world = WorldSheet.GetRow(TellTarget.World)?.Name?.RawString ?? "???"; + var world = Sheets.WorldSheet.GetRow(TellTarget.World)?.Name?.RawString ?? "???"; channelNameChunks = [ @@ -829,7 +830,7 @@ public sealed class ChatLogWindow : Window // Note: don't use HidePlayerInString here because // abbreviation settings do not affect this. var playerName = HashPlayer(tellPlayerName, tellWorldId); - var world = WorldSheet.GetRow(tellWorldId)?.Name?.RawString ?? "???"; + var world = Sheets.WorldSheet.GetRow(tellWorldId)?.Name?.RawString ?? "???"; channelNameChunks = [ @@ -909,7 +910,7 @@ public sealed class ChatLogWindow : Window { var target = TellTarget; var reason = target.Reason; - var world = WorldSheet.GetRow(target.World); + var world = Sheets.WorldSheet.GetRow(target.World); if (world is { IsPublic: true }) { if (reason == TellReason.Reply && GameFunctions.GameFunctions.GetFriends().Any(friend => friend.ContentId == target.ContentId)) @@ -1571,7 +1572,7 @@ public sealed class ChatLogWindow : Window if (text.StartsWith('/')) { var command = text.Split(' ')[0]; - var cmd = TextCommandSheet.FirstOrDefault(cmd => + var cmd = Sheets.TextCommandSheet.FirstOrDefault(cmd => cmd.Command.RawString == command || cmd.Alias.RawString == command || cmd.ShortCommand.RawString == command || cmd.ShortAlias.RawString == command);