- Identify web payloads better

- Switch to IconId field name
- Add unique id to every message
- Automate nodejs build step via csproj
- Add unread color to tab opener
- Add unread number to tab name
- Update ImageSharp dep
This commit is contained in:
Infi
2025-11-17 17:48:53 +01:00
parent 0ab2d15a87
commit 4b94c6e30e
12 changed files with 121 additions and 59 deletions
-4
View File
@@ -38,10 +38,6 @@
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath> <HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina"> <Reference Include="Lumina">
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath> <HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
<Private>false</Private> <Private>false</Private>
+12 -11
View File
@@ -1,6 +1,6 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0"> <Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup> <PropertyGroup>
<Version>1.31.2</Version> <Version>1.32.0</Version>
<TargetFramework>net9.0-windows</TargetFramework> <TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -17,7 +17,7 @@
<PackageReference Include="MessagePack" Version="3.1.4" /> <PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Include="Pidgin" Version="3.3.0" /> <PackageReference Include="Pidgin" Version="3.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Watson.Lite" Version="6.3.9" /> <PackageReference Include="Watson.Lite" Version="6.3.9" />
</ItemGroup> </ItemGroup>
@@ -40,15 +40,16 @@
<Folder Include="images\" /> <Folder Include="images\" />
</ItemGroup> </ItemGroup>
<Target Name="NodeJS Compile" BeforeTargets="BeforeCompile">
<Exec Command="npm install" WorkingDirectory="Http\Frontend"/>
<Exec Command="npm run build" WorkingDirectory="Http\Frontend"/>
</Target>
<Target Name="CopyFiles" AfterTargets="Build">
<ItemGroup> <ItemGroup>
<Content Include="Http\static\*.*"> <Files Include="$(MSBuildThisFileDirectory)\Http\Frontend\build\**" />
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Http\templates\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Http\Frontend\build\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<Copy SourceFiles="@(Files)" DestinationFolder="$(TargetDir)\Frontend\%(RecursiveDir)" />
</Target>
</Project> </Project>
@@ -25,9 +25,10 @@
} }
function ontransitionend() { function ontransitionend() {
if (scrolledToBottom) if (scrolledToBottom) {
scrollMessagesToBottom(); scrollMessagesToBottom();
} }
}
</script> </script>
<aside <aside
@@ -50,9 +51,9 @@
<ol id="tabs-list"> <ol id="tabs-list">
{#each knownTabs as tab} {#each knownTabs as tab}
<li class:active={selectedTab.index == tab.index}> <li class:active={selectedTab.index === tab.index}>
<button type="button" onclick={() => selectTab(tab.index)}> <button type="button" onclick={() => selectTab(tab.index)}>
{ tab.name } { tab.name } {tab.unreadCount > 0 ? `(${tab.unreadCount})`: '' }
</button> </button>
</li> </li>
{/each} {/each}
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { tabPaneState, tabPaneAnimationState, openTabPane } from "$lib/shared.svelte"; import {tabPaneState, tabPaneAnimationState, openTabPane, knownTabs} from "$lib/shared.svelte";
function onclick() { function onclick() {
tabPaneAnimationState.noAnimation = false; tabPaneAnimationState.noAnimation = false;
@@ -7,7 +7,7 @@
} }
</script> </script>
<button type="button" aria-label="Open tab pane" class:visible={!tabPaneState.visible} {onclick} disabled={tabPaneState.visible}> <button type="button" aria-label="Open tab pane" class:visible={!tabPaneState.visible} class:unread={knownTabs.some((tab) => tab.unreadCount > 0)} {onclick} disabled={tabPaneState.visible}>
<!-- "chevron-right" icon from https://github.com/feathericons/feather, under MIT license --> <!-- "chevron-right" icon from https://github.com/feathericons/feather, under MIT license -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button> </button>
+20 -13
View File
@@ -1,4 +1,5 @@
import { channelOptions, isChannelLocked, selectedTab, knownTabs, chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte"; import { channelOptions, isChannelLocked, selectedTab, knownTabs, chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
import { WebPayloadType } from "$lib/payload";
import { source, type Source } from "sveltekit-sse"; import { source, type Source } from "sveltekit-sse";
interface ChatElements { interface ChatElements {
@@ -17,15 +18,16 @@ interface Messages {
// ref `DataStructure.MessageResponse` // ref `DataStructure.MessageResponse`
interface MessageResponse { interface MessageResponse {
id: string;
timestamp: string; timestamp: string;
templates: Template[]; templates: Template[];
} }
// ref `DataStructure.MessageTemplate` // ref `DataStructure.MessageTemplate`
interface Template { interface Template {
id: number; payloadType: WebPayloadType;
payload: string;
content: string; content: string;
iconId: number;
color: number; color: number;
} }
@@ -73,9 +75,6 @@ export class ChatTwoWeb {
setupDOMElements() { setupDOMElements() {
this.elements = { this.elements = {
// channelHint: document.getElementById('channel-hint'),
// channelSelect: document.getElementById('channel-select'),
messagesContainer: document.querySelector('#messages > .scroll-container')!, messagesContainer: document.querySelector('#messages > .scroll-container')!,
messagesList: document.getElementById('messages-list'), messagesList: document.getElementById('messages-list'),
@@ -209,20 +208,20 @@ export class ChatTwoWeb {
for( const template of templates ) { for( const template of templates ) {
const spanElement = document.createElement('span'); const spanElement = document.createElement('span');
switch (template.payload) { switch (template.payloadType) {
case 'text': case WebPayloadType.RawText:
this.processTextTemplate(template, spanElement); this.processTextTemplate(template, spanElement);
break; break;
case 'url': case WebPayloadType.CustomUri:
this.processUrlTemplate(template, spanElement); this.processUrlTemplate(template, spanElement);
break; break;
case 'emote': case WebPayloadType.CustomEmote:
this.processEmote(template, spanElement); this.processEmote(template, spanElement);
break; break;
case 'icon': case WebPayloadType.Icon:
this.processIcon(template, spanElement); this.processIcon(template, spanElement);
break; break;
case 'empty': default:
continue; continue;
} }
@@ -272,7 +271,7 @@ export class ChatTwoWeb {
processIcon(template: Template, spanElement: HTMLSpanElement) { processIcon(template: Template, spanElement: HTMLSpanElement) {
spanElement.classList.add('gfd-icon'); spanElement.classList.add('gfd-icon');
spanElement.classList.add(`gfd-icon-hq-${template.id}`); spanElement.classList.add(`gfd-icon-hq-${template.iconId}`);
} }
clearAllMessages() { clearAllMessages() {
@@ -378,10 +377,18 @@ export class ChatTwoWeb {
// the unread state of a specific tab has changed // the unread state of a specific tab has changed
this.connection.select('tab-unread-state').subscribe((data: string) => { this.connection.select('tab-unread-state').subscribe((data: string) => {
console.log(`tab-unread-state: ${data}`) console.log(`tab-unread-state`, data)
if (data) { if (data) {
try { try {
const chatTabUnreadState: ChatTabUnreadState = JSON.parse(data); const chatTabUnreadState: ChatTabUnreadState = JSON.parse(data);
let tab = knownTabs.find((tab) => tab.index === chatTabUnreadState.index);
if (tab) {
tab.unreadCount = chatTabUnreadState.unreadCount;
}
else {
console.error("Unable to find tab!")
console.error(chatTabUnreadState)
}
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
+25
View File
@@ -0,0 +1,25 @@
export enum WebPayloadType {
// Dalamud
Unknown,
Player,
Item,
Status,
RawText,
UIForeground,
UIGlow,
MapLink,
AutoTranslateText,
EmphasisItalic,
Icon,
Quest,
DalamudLink,
NewLine,
SeHyphen,
PartyFinder,
// Custom
CustomPartyFinder = 0x50,
CustomAchievement = 0x51,
CustomUri = 0x52,
CustomEmote = 0x53,
}
+1 -1
View File
@@ -18,7 +18,7 @@ public class HostContext
public readonly List<SSEConnection> EventConnections = []; public readonly List<SSEConnection> EventConnections = [];
public readonly CancellationTokenSource TokenSource = new(); public readonly CancellationTokenSource TokenSource = new();
public readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Http/Frontend/build"); public readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Frontend/");
public HostContext(ServerCore core) public HostContext(ServerCore core)
{ {
+6 -12
View File
@@ -61,6 +61,7 @@ public struct Messages(MessageResponse[] set)
/// </summary> /// </summary>
public struct MessageResponse() public struct MessageResponse()
{ {
[JsonProperty("id")] public Guid Id = Guid.NewGuid();
[JsonProperty("timestamp")] public string Timestamp = ""; [JsonProperty("timestamp")] public string Timestamp = "";
[JsonProperty("templates")] public MessageTemplate[] Templates; [JsonProperty("templates")] public MessageTemplate[] Templates;
} }
@@ -71,17 +72,10 @@ public struct MessageResponse()
public struct MessageTemplate() public struct MessageTemplate()
{ {
/// <summary> /// <summary>
/// Template type /// The type of payload.
/// /// Dalamuds enum is just a baseline, there exists more that are expressed through raw values.
/// icon = a game icon
/// emote = BetterTTV emote
/// url = Simple url that should be clickable
/// text = Simple text content of the message
///
/// Note:
/// Empty is used for invalid payloads
/// </summary> /// </summary>
[JsonProperty("payload")] public required string Payload; [JsonProperty("payloadType")] public WebPayloadType PayloadType = WebPayloadType.Unknown;
/// <summary> /// <summary>
/// Used for text and emote. /// Used for text and emote.
@@ -91,7 +85,7 @@ public struct MessageTemplate()
/// <summary> /// <summary>
/// Used for an icon. /// Used for an icon.
/// </summary> /// </summary>
[JsonProperty("id")] public uint Id; [JsonProperty("iconId")] public uint IconId;
/// <summary> /// <summary>
/// Used for text and url /// Used for text and url
@@ -101,7 +95,7 @@ public struct MessageTemplate()
/// </summary> /// </summary>
[JsonProperty("color")] public uint Color; [JsonProperty("color")] public uint Color;
public static MessageTemplate Empty => new() {Payload = "empty"}; public static MessageTemplate Empty => new();
} }
#endregion #endregion
@@ -0,0 +1,31 @@
namespace ChatTwo.Http.MessageProtocol;
/// <summary>
/// Baseline: <see cref="Dalamud.Game.Text.SeStringHandling.PayloadType"/>
/// </summary>
public enum WebPayloadType
{
// Dalamud
Unknown,
Player,
Item,
Status,
RawText,
UIForeground,
UIGlow,
MapLink,
AutoTranslateText,
EmphasisItalic,
Icon,
Quest,
DalamudLink,
NewLine,
SeHyphen,
PartyFinder,
// Custom
CustomPartyFinder = 0x50,
CustomAchievement = 0x51,
CustomUri = 0x52,
CustomEmote = 0x53,
}
+3 -3
View File
@@ -46,7 +46,7 @@ public class Processing
if (chunk is IconChunk { } icon) if (chunk is IconChunk { } icon)
{ {
var iconId = (uint)icon.Icon; var iconId = (uint)icon.Icon;
return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {Payload = "icon", Id = iconId}: MessageTemplate.Empty; return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {PayloadType = WebPayloadType.Icon, IconId = iconId}: MessageTemplate.Empty;
} }
if (chunk is TextChunk { } text) if (chunk is TextChunk { } text)
@@ -56,7 +56,7 @@ public class Processing
var image = EmoteCache.GetEmote(emotePayload.Code); var image = EmoteCache.GetEmote(emotePayload.Code);
if (image is { Failed: false }) if (image is { Failed: false })
return new MessageTemplate { Payload = "emote", Color = 0, Content = emotePayload.Code }; return new MessageTemplate { PayloadType = WebPayloadType.CustomEmote, Color = 0, Content = emotePayload.Code };
} }
var color = text.Foreground; var color = text.Foreground;
@@ -78,7 +78,7 @@ public class Processing
} }
var isNotUrl = text.Link is not UriPayload; var isNotUrl = text.Link is not UriPayload;
return new MessageTemplate { Payload = isNotUrl ? "text" : "url", Color = color.Value, Content = userContent }; return new MessageTemplate { PayloadType = isNotUrl ? WebPayloadType.RawText : WebPayloadType.CustomUri, Color = color.Value, Content = userContent };
} }
return MessageTemplate.Empty; return MessageTemplate.Empty;
+7
View File
@@ -217,6 +217,8 @@ internal class MessageManager : IAsyncDisposable
// this will be called for each message immediately after ChatMessage is // this will be called for each message immediately after ChatMessage is
// called for each message. // called for each message.
private unsafe void ContentIdResolver(RaptureLogModule* agent, ulong contentId, ulong accountId, int messageIndex, ushort worldId, ushort chatType) private unsafe void ContentIdResolver(RaptureLogModule* agent, ulong contentId, ulong accountId, int messageIndex, ushort worldId, ushort chatType)
{
try
{ {
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType); ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
if (PendingSync.Count == 0) if (PendingSync.Count == 0)
@@ -225,6 +227,11 @@ internal class MessageManager : IAsyncDisposable
PendingSync.Last().ContentId = contentId; PendingSync.Last().ContentId = contentId;
PendingSync.Last().AccountId = accountId; PendingSync.Last().AccountId = accountId;
} }
catch (Exception ex)
{
Plugin.Log.Error(ex, "Error in ContentIdResolver");
}
}
private void ProcessMessage(PendingMessage pendingMessage) private void ProcessMessage(PendingMessage pendingMessage)
{ {
+3 -3
View File
@@ -44,9 +44,9 @@
}, },
"SixLabors.ImageSharp": { "SixLabors.ImageSharp": {
"type": "Direct", "type": "Direct",
"requested": "[3.1.11, )", "requested": "[3.1.12, )",
"resolved": "3.1.11", "resolved": "3.1.12",
"contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" "contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
}, },
"Watson.Lite": { "Watson.Lite": {
"type": "Direct", "type": "Direct",