- 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>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
<Private>false</Private>
+12 -11
View File
@@ -1,6 +1,6 @@
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup>
<Version>1.31.2</Version>
<Version>1.32.0</Version>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -17,7 +17,7 @@
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.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" />
</ItemGroup>
@@ -40,15 +40,16 @@
<Folder Include="images\" />
</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>
<Content Include="Http\static\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Http\templates\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Http\Frontend\build\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Files Include="$(MSBuildThisFileDirectory)\Http\Frontend\build\**" />
</ItemGroup>
<Copy SourceFiles="@(Files)" DestinationFolder="$(TargetDir)\Frontend\%(RecursiveDir)" />
</Target>
</Project>
@@ -25,9 +25,10 @@
}
function ontransitionend() {
if (scrolledToBottom)
if (scrolledToBottom) {
scrollMessagesToBottom();
}
}
</script>
<aside
@@ -50,9 +51,9 @@
<ol id="tabs-list">
{#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)}>
{ tab.name }
{ tab.name } {tab.unreadCount > 0 ? `(${tab.unreadCount})`: '' }
</button>
</li>
{/each}
@@ -1,5 +1,5 @@
<script lang="ts">
import { tabPaneState, tabPaneAnimationState, openTabPane } from "$lib/shared.svelte";
import {tabPaneState, tabPaneAnimationState, openTabPane, knownTabs} from "$lib/shared.svelte";
function onclick() {
tabPaneAnimationState.noAnimation = false;
@@ -7,7 +7,7 @@
}
</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 -->
<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>
+20 -13
View File
@@ -1,4 +1,5 @@
import { channelOptions, isChannelLocked, selectedTab, knownTabs, chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
import { WebPayloadType } from "$lib/payload";
import { source, type Source } from "sveltekit-sse";
interface ChatElements {
@@ -17,15 +18,16 @@ interface Messages {
// ref `DataStructure.MessageResponse`
interface MessageResponse {
id: string;
timestamp: string;
templates: Template[];
}
// ref `DataStructure.MessageTemplate`
interface Template {
id: number;
payload: string;
payloadType: WebPayloadType;
content: string;
iconId: number;
color: number;
}
@@ -73,9 +75,6 @@ export class ChatTwoWeb {
setupDOMElements() {
this.elements = {
// channelHint: document.getElementById('channel-hint'),
// channelSelect: document.getElementById('channel-select'),
messagesContainer: document.querySelector('#messages > .scroll-container')!,
messagesList: document.getElementById('messages-list'),
@@ -209,20 +208,20 @@ export class ChatTwoWeb {
for( const template of templates ) {
const spanElement = document.createElement('span');
switch (template.payload) {
case 'text':
switch (template.payloadType) {
case WebPayloadType.RawText:
this.processTextTemplate(template, spanElement);
break;
case 'url':
case WebPayloadType.CustomUri:
this.processUrlTemplate(template, spanElement);
break;
case 'emote':
case WebPayloadType.CustomEmote:
this.processEmote(template, spanElement);
break;
case 'icon':
case WebPayloadType.Icon:
this.processIcon(template, spanElement);
break;
case 'empty':
default:
continue;
}
@@ -272,7 +271,7 @@ export class ChatTwoWeb {
processIcon(template: Template, spanElement: HTMLSpanElement) {
spanElement.classList.add('gfd-icon');
spanElement.classList.add(`gfd-icon-hq-${template.id}`);
spanElement.classList.add(`gfd-icon-hq-${template.iconId}`);
}
clearAllMessages() {
@@ -378,10 +377,18 @@ export class ChatTwoWeb {
// the unread state of a specific tab has changed
this.connection.select('tab-unread-state').subscribe((data: string) => {
console.log(`tab-unread-state: ${data}`)
console.log(`tab-unread-state`, data)
if (data) {
try {
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) {
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 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)
{
+6 -12
View File
@@ -61,6 +61,7 @@ public struct Messages(MessageResponse[] set)
/// </summary>
public struct MessageResponse()
{
[JsonProperty("id")] public Guid Id = Guid.NewGuid();
[JsonProperty("timestamp")] public string Timestamp = "";
[JsonProperty("templates")] public MessageTemplate[] Templates;
}
@@ -71,17 +72,10 @@ public struct MessageResponse()
public struct MessageTemplate()
{
/// <summary>
/// Template type
///
/// 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
/// The type of payload.
/// Dalamuds enum is just a baseline, there exists more that are expressed through raw values.
/// </summary>
[JsonProperty("payload")] public required string Payload;
[JsonProperty("payloadType")] public WebPayloadType PayloadType = WebPayloadType.Unknown;
/// <summary>
/// Used for text and emote.
@@ -91,7 +85,7 @@ public struct MessageTemplate()
/// <summary>
/// Used for an icon.
/// </summary>
[JsonProperty("id")] public uint Id;
[JsonProperty("iconId")] public uint IconId;
/// <summary>
/// Used for text and url
@@ -101,7 +95,7 @@ public struct MessageTemplate()
/// </summary>
[JsonProperty("color")] public uint Color;
public static MessageTemplate Empty => new() {Payload = "empty"};
public static MessageTemplate Empty => new();
}
#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)
{
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)
@@ -56,7 +56,7 @@ public class Processing
var image = EmoteCache.GetEmote(emotePayload.Code);
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;
@@ -78,7 +78,7 @@ public class Processing
}
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;
+7
View File
@@ -217,6 +217,8 @@ internal class MessageManager : IAsyncDisposable
// this will be called for each message immediately after ChatMessage is
// called for each message.
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);
if (PendingSync.Count == 0)
@@ -225,6 +227,11 @@ internal class MessageManager : IAsyncDisposable
PendingSync.Last().ContentId = contentId;
PendingSync.Last().AccountId = accountId;
}
catch (Exception ex)
{
Plugin.Log.Error(ex, "Error in ContentIdResolver");
}
}
private void ProcessMessage(PendingMessage pendingMessage)
{
+3 -3
View File
@@ -44,9 +44,9 @@
},
"SixLabors.ImageSharp": {
"type": "Direct",
"requested": "[3.1.11, )",
"resolved": "3.1.11",
"contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw=="
"requested": "[3.1.12, )",
"resolved": "3.1.12",
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
},
"Watson.Lite": {
"type": "Direct",