preserve chat replies and threads

This commit is contained in:
2026-10-25 05:30:51 +01:00
parent 46fb8e2cc3
commit 953dfaaad0
2 changed files with 59 additions and 2 deletions
+29 -2
View File
@@ -23,6 +23,7 @@
let activityFilter: 'all' | 'moderation' = 'all';
let chatMessages: Array<{ event_id: string; message_id: string; user_id: string; user_name: string; text: string; fragments: Array<{ type: string; text: string; emote_id?: string }>; sent_at: string; reply_parent_message_id?: string }> = [];
let chatReplyParent: { message_id: string; user_name: string } | undefined;
let chatThreadRoot: { message_id: string; user_name: string; text: string } | undefined;
let pinnedMessageIds: string[] = [];
let notifications: Array<{ id: string; severity: string; title: string; message: string; seen_at: string | null; created_at: string }> = [];
let showNotifications = false;
@@ -226,6 +227,7 @@
collaborationInvites = [];
showCollaboration = false;
chatReplyParent = undefined;
chatThreadRoot = undefined;
pinnedMessageIds = [];
}
@@ -243,12 +245,12 @@
const event: { event_type?: string } = JSON.parse(message.data);
if (event.event_type === 'connection.ready') realtimeState = 'CONNECTED';
else if ('event_id' in event && event.event_type) {
const activity = event as { event_id: string; event_type: string; occurred_at: string; payload?: { message_id?: string; chatter_user_id?: string; chatter_user_name?: string; message?: { text?: string; fragments?: Array<{ type: string; text: string; emote?: { id?: string } }> } } };
const activity = event as { event_id: string; event_type: string; occurred_at: string; payload?: { message_id?: string; chatter_user_id?: string; chatter_user_name?: string; message?: { text?: string; fragments?: Array<{ type: string; text: string; emote?: { id?: string } }> }; reply?: { parent_message_id?: string } } };
if (!activityEvents.some((item) => item.event_id === activity.event_id)) {
activityEvents = [activity, ...activityEvents].slice(0, 100);
}
if (activity.event_type === 'twitch.channel.chat.message' && activity.payload?.message?.text) {
chatMessages = [{ event_id: activity.event_id, message_id: activity.payload.message_id ?? activity.event_id, user_id: activity.payload.chatter_user_id ?? '', user_name: activity.payload.chatter_user_name ?? 'unknown', text: activity.payload.message.text, fragments: (activity.payload.message.fragments ?? []).map((fragment) => ({ type: fragment.type, text: fragment.text, emote_id: fragment.emote?.id })), sent_at: activity.occurred_at }, ...chatMessages].slice(0, 100);
chatMessages = [{ event_id: activity.event_id, message_id: activity.payload.message_id ?? activity.event_id, user_id: activity.payload.chatter_user_id ?? '', user_name: activity.payload.chatter_user_name ?? 'unknown', text: activity.payload.message.text, fragments: (activity.payload.message.fragments ?? []).map((fragment) => ({ type: fragment.type, text: fragment.text, emote_id: fragment.emote?.id })), sent_at: activity.occurred_at, reply_parent_message_id: activity.payload.reply?.parent_message_id }, ...chatMessages].slice(0, 100);
}
}
} catch {
@@ -551,6 +553,10 @@
chatReplyParent = { message_id: message.message_id, user_name: message.user_name };
}
function openChatThread(message: { message_id: string; user_name: string; text: string }) {
chatThreadRoot = { message_id: message.message_id, user_name: message.user_name, text: message.text };
}
function moderateChatUser(userId: string) {
if (!userId) return;
moderationUserId = userId;
@@ -911,5 +917,26 @@
</div>
</section>
{/if}
{#if chatMessages.some((message) => message.reply_parent_message_id)}
<section class="panel thread-index-panel">
<div class="panel-header"><span>CHAT THREADS</span></div>
{#each chatMessages.filter((message) => message.reply_parent_message_id) as reply}
{@const root = chatMessages.find((message) => message.message_id === reply.reply_parent_message_id)}
{#if root}<button class="thread-index-row" onclick={() => openChatThread(root)}><span>{root.user_name}: {root.text}</span><span class="micro">OPEN THREAD</span></button>{/if}
{/each}
</section>
{/if}
{#if chatThreadRoot}
<section class="panel chat-thread-panel">
<div class="panel-header"><span>THREAD · @{chatThreadRoot.user_name}</span><button class="secondary" onclick={() => (chatThreadRoot = undefined)}>CLOSE</button></div>
<div class="thread-root"><strong>{chatThreadRoot.user_name}</strong><span>{chatThreadRoot.text}</span></div>
<div class="thread-replies">
{#each chatMessages.filter((message) => message.reply_parent_message_id === chatThreadRoot?.message_id) as reply}
<div class="thread-reply"><strong>{reply.user_name}</strong><span>{reply.text}</span></div>
{:else}<div class="empty-row">No replies received yet.</div>{/each}
</div>
<button class="primary" onclick={() => chatThreadRoot && replyToChatMessage(chatThreadRoot)}>REPLY IN THREAD</button>
</section>
{/if}
</main>
</div>
+30
View File
@@ -901,3 +901,33 @@ label {
min-width: 28px;
padding: 2px 6px;
}
.chat-thread-panel,
.thread-index-panel {
margin-top: 10px;
}
.thread-index-row,
.thread-reply {
display: flex;
justify-content: space-between;
gap: 12px;
width: 100%;
padding: 8px;
border: 0;
border-bottom: 1px solid var(--ui-divider);
background: transparent;
color: var(--ed-fg);
text-align: left;
}
.thread-index-row:hover {
background: var(--ui-surface-hi);
}
.thread-root {
display: flex;
gap: 8px;
padding: 10px;
border-bottom: 1px solid var(--ui-line-focus);
}
.thread-replies {
margin: 6px 0 10px 18px;
border-left: 2px solid var(--ui-line-focus);
}