diff --git a/src/lib/commands/built-in/founders.ts b/src/lib/commands/built-in/founders.ts
index 143a1c8b..63f84555 100644
--- a/src/lib/commands/built-in/founders.ts
+++ b/src/lib/commands/built-in/founders.ts
@@ -1,5 +1,4 @@
import { foundersQuery } from "$lib/graphql/twitch";
-import { SystemMessage } from "$lib/models/message/system-message";
import { defineCommand } from "../util";
export default defineCommand({
@@ -7,8 +6,6 @@ export default defineCommand({
name: "founders",
description: "Display a list of founders for this channel",
async exec(_, channel) {
- const message = new SystemMessage(channel);
-
const { user } = await channel.client.send(foundersQuery, { id: channel.id });
const founders =
@@ -16,12 +13,10 @@ export default defineCommand({
?.flatMap((founder) => (founder?.user ? [founder.user.displayName] : []))
.toSorted() ?? [];
- if (!founders.length) {
- message.text = "This channel has no founders.";
- } else {
- message.text = `Channel founders (${founders.length}): ${founders.join(", ")}`;
- }
+ const text = founders.length
+ ? `Channel founders (${founders.length}): ${founders.join(", ")}`
+ : "This channel has no founders.";
- channel.chat.addMessage(message);
+ channel.chat.notice(text);
},
});
diff --git a/src/lib/commands/built-in/reload-badges.ts b/src/lib/commands/built-in/reload-badges.ts
index 7de470cf..3381df23 100644
--- a/src/lib/commands/built-in/reload-badges.ts
+++ b/src/lib/commands/built-in/reload-badges.ts
@@ -30,6 +30,6 @@ export default defineCommand({
await Promise.all(promises);
}
- channel.chat.addSystemMessage("Reloaded badges.");
+ channel.chat.notice("Reloaded badges.");
},
});
diff --git a/src/lib/commands/built-in/reload-cheermotes.ts b/src/lib/commands/built-in/reload-cheermotes.ts
index 5e1de312..b0828997 100644
--- a/src/lib/commands/built-in/reload-cheermotes.ts
+++ b/src/lib/commands/built-in/reload-cheermotes.ts
@@ -7,6 +7,6 @@ export default defineCommand({
async exec(_, channel) {
await channel.fetchCheermotes(true);
- channel.chat.addSystemMessage("Reloaded cheermotes.");
+ channel.chat.notice("Reloaded cheermotes.");
},
});
diff --git a/src/lib/commands/built-in/reload-emotes.ts b/src/lib/commands/built-in/reload-emotes.ts
index 54c4eeb7..83b0368f 100644
--- a/src/lib/commands/built-in/reload-emotes.ts
+++ b/src/lib/commands/built-in/reload-emotes.ts
@@ -18,6 +18,6 @@ export default defineCommand({
await app.emotes.fetch(includeGlobal);
await channel.emotes.fetch(true);
- channel.chat.addSystemMessage("Reloaded emotes.");
+ channel.chat.notice("Reloaded emotes.");
},
});
diff --git a/src/lib/commands/built-in/reload-theme.ts b/src/lib/commands/built-in/reload-theme.ts
index b19e02c0..4bcc0bdf 100644
--- a/src/lib/commands/built-in/reload-theme.ts
+++ b/src/lib/commands/built-in/reload-theme.ts
@@ -10,6 +10,6 @@ export default defineCommand({
await loadThemes(settings.state["appearance.theme"]);
await injectTheme(settings.state["appearance.theme"]);
- channel.chat.addSystemMessage("Reloaded theme.");
+ channel.chat.notice("Reloaded theme.");
},
});
diff --git a/src/lib/commands/twitch/block.ts b/src/lib/commands/twitch/block.ts
index 1101e543..bc2ef41f 100644
--- a/src/lib/commands/twitch/block.ts
+++ b/src/lib/commands/twitch/block.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import BlockStatus from "$lib/components/message/events/BlockStatus.svelte";
import { defineCommand, getTarget } from "../util";
export default defineCommand({
@@ -12,10 +13,6 @@ export default defineCommand({
await app.twitch.users.block(target.id);
- channel.chat.addSystemMessage({
- type: "blockStatus",
- blocked: true,
- user: target.user,
- });
+ channel.chat.event(BlockStatus, { blocked: true, user: target.user });
},
});
diff --git a/src/lib/commands/twitch/marker.ts b/src/lib/commands/twitch/marker.ts
index 7dd49cfe..3b1777ee 100644
--- a/src/lib/commands/twitch/marker.ts
+++ b/src/lib/commands/twitch/marker.ts
@@ -27,6 +27,6 @@ export default defineCommand({
const echo = description ? `: ${description}` : "";
- channel.chat.addSystemMessage(`Stream marker created at ${duration.format(format) + echo}`);
+ channel.chat.notice(`Stream marker created at ${duration.format(format) + echo}`);
},
});
diff --git a/src/lib/commands/twitch/mods.ts b/src/lib/commands/twitch/mods.ts
index d9b4ee64..13a7de4b 100644
--- a/src/lib/commands/twitch/mods.ts
+++ b/src/lib/commands/twitch/mods.ts
@@ -1,5 +1,4 @@
import { modsQuery } from "$lib/graphql/twitch";
-import { SystemMessage } from "$lib/models/message/system-message";
import { defineCommand } from "../util";
export default defineCommand({
@@ -7,8 +6,6 @@ export default defineCommand({
name: "mods",
description: "Display a list of moderators for this channel",
async exec(_, channel) {
- const message = new SystemMessage(channel);
-
const { user } = await channel.client.send(modsQuery, { id: channel.id });
const mods =
@@ -16,12 +13,10 @@ export default defineCommand({
.flatMap((edge) => (edge.node ? [edge.node.displayName] : []))
.toSorted() ?? [];
- if (!mods.length) {
- message.text = "This channel has no moderators.";
- } else {
- message.text = `Channel moderators (${mods.length}): ${mods.join(", ")}`;
- }
+ const text = mods.length
+ ? `Channel moderators (${mods.length}): ${mods.join(", ")}`
+ : "This channel has no moderators.";
- channel.chat.addMessage(message);
+ channel.chat.notice(text);
},
});
diff --git a/src/lib/commands/twitch/unblock.ts b/src/lib/commands/twitch/unblock.ts
index d6973a97..a10e82ff 100644
--- a/src/lib/commands/twitch/unblock.ts
+++ b/src/lib/commands/twitch/unblock.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import BlockStatus from "$lib/components/message/events/BlockStatus.svelte";
import { defineCommand, getTarget } from "../util";
export default defineCommand({
@@ -11,10 +12,6 @@ export default defineCommand({
await app.twitch.users.unblock(target.id);
- channel.chat.addSystemMessage({
- type: "blockStatus",
- blocked: false,
- user: target.user,
- });
+ channel.chat.event(BlockStatus, { blocked: false, user: target.user });
},
});
diff --git a/src/lib/commands/twitch/vips.ts b/src/lib/commands/twitch/vips.ts
index eeea7af5..df310338 100644
--- a/src/lib/commands/twitch/vips.ts
+++ b/src/lib/commands/twitch/vips.ts
@@ -1,5 +1,4 @@
import { vipsQuery } from "$lib/graphql/twitch";
-import { SystemMessage } from "$lib/models/message/system-message";
import { defineCommand } from "../util";
export default defineCommand({
@@ -7,8 +6,6 @@ export default defineCommand({
name: "vips",
description: "Display a list of VIPs for this channel",
async exec(_, channel) {
- const message = new SystemMessage(channel);
-
const { user } = await channel.client.send(vipsQuery, { id: channel.id });
const vips =
@@ -16,12 +13,10 @@ export default defineCommand({
?.flatMap((edge) => (edge.node ? [edge.node.displayName] : []))
.toSorted() ?? [];
- if (!vips.length) {
- message.text = "This channel has no VIPs.";
- } else {
- message.text = `Channel VIPs (${vips.length}): ${vips.join(", ")}`;
- }
+ const text = vips.length
+ ? `Channel VIPs (${vips.length}): ${vips.join(", ")}`
+ : "This channel has no VIPs.";
- channel.chat.addMessage(message);
+ channel.chat.notice(text);
},
});
diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte
index 98770cfb..7cbb794f 100644
--- a/src/lib/components/chat/Chat.svelte
+++ b/src/lib/components/chat/Chat.svelte
@@ -4,8 +4,8 @@
import type { Message } from "$lib/models/message/message";
import { settings } from "$lib/settings";
import AutoMod from "../message/AutoMod.svelte";
+ import Event from "../message/Event.svelte";
import Notification from "../message/Notification.svelte";
- import SystemMessage from "../message/SystemMessage.svelte";
import UserMessage from "../message/UserMessage.svelte";
import Separator from "./Separator.svelte";
@@ -101,7 +101,7 @@
bind:this={list}
>
{#snippet children(message, i)}
- {#if message.isSystem() || message.isUser()}
+ {#if message.isEvent() || message.isUser()}
{@const prev = chat.messages[i - 1]}
{@const isNewDay = prev && prev.timestamp.getDate() !== message.timestamp.getDate()}
@@ -118,8 +118,8 @@
{/if}
- {#if message.isSystem()}
-
+ {#if message.isEvent()}
+
{:else if message.isUser()}
{#if message.event}
@@ -131,7 +131,7 @@
{/if}
{@const next = chat.messages.at(i + 1)}
- {@const nextRecent = next && (next.isSystem() || next.isUser()) && next.recent}
+ {@const nextRecent = next && (next.isEvent() || next.isUser()) && next.recent}
{#if message === lastRead && next && settings.state["chat.newSeparator"]}
New messages
diff --git a/src/lib/components/message/Event.svelte b/src/lib/components/message/Event.svelte
new file mode 100644
index 00000000..5adb4d31
--- /dev/null
+++ b/src/lib/components/message/Event.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/src/lib/components/message/SystemMessage.svelte b/src/lib/components/message/SystemMessage.svelte
deleted file mode 100644
index 9866eb1e..00000000
--- a/src/lib/components/message/SystemMessage.svelte
+++ /dev/null
@@ -1,234 +0,0 @@
-
-
-
-
-
-
- {#if ctx}
- {#if ctx.type === "blockStatus"}
- {ctx.blocked ? "Blocked" : "Unblocked"} {@html colorizeName(ctx.user)}
- {:else if ctx.type === "join"}
- Joined {@html colorizeName(message.channel.user)}
- {:else if ctx.type === "raid"}
- {@html colorizeName(ctx.moderator)} is raiding {@html colorizeName(ctx.user)} with {@html ctx.viewers}
- viewers.
- {:else if ctx.type === "unraid"}
- {@html colorizeName(ctx.moderator)} canceled the raid on {@html colorizeName(
- ctx.user,
- )}.
- {:else if ctx.type === "untimeout"}
- {@html colorizeName(ctx.moderator)} removed timeout on {@html colorizeName(
- ctx.viewer,
- )}.
- {:else if ctx.type === "warnAck"}
- {@html colorizeName(ctx.viewer)} acknowledged their warning.
- {:else}
- {@render snippetMap[ctx.type](ctx as never)}
- {/if}
- {:else}
- {message.text}
- {/if}
-
-
-
-{#snippet autoMod(ctx: AutoModContext)}
- {@const target = colorizeName(ctx.viewer)}
-
- {#if ctx.status === "expired"}
- {@html target}'s message expired and was not shown in chat.
- {:else}
- {@html colorizeName(ctx.moderator)} {ctx.status} {@html target}'s message.
- {/if}
-{/snippet}
-
-{#snippet banned()}
- You are permanently banned from {@html colorizeName(message.channel.user)} and cannot send messages.
- If you have been unbanned, try
- .
-{/snippet}
-
-{#snippet banStatus(ctx: BanStatusContext)}
- {@const target = colorizeName(ctx.viewer)}
- {@const action = ctx.banned ? "banned" : "unbanned"}
-
- {#if ctx.moderator}
- {@html colorizeName(ctx.moderator)} {action} {@html target}
- {:else}
- {@html target} has been {action}
- {/if}{ctx.reason ? `: ${ctx.reason}` : "."}
-{/snippet}
-
-{#snippet clear(ctx: ClearContext)}
- {#if ctx.moderator}
- {@html colorizeName(ctx.moderator)} cleared the chat
- {:else}
- The chat has been cleared
- {/if}
-
- for non-moderator viewers.
-{/snippet}
-
-{#snippet deleteMsg(ctx: DeleteContext)}
- {@const target = colorizeName(ctx.user)}
-
- {#if ctx.moderator}
- {@html colorizeName(ctx.moderator)} deleted {@html target}'s message: {ctx.text}
- {:else}
- {@html target}'s message was deleted: {ctx.text}
- {/if}
-{/snippet}
-
-{#snippet emoteSetChange(ctx: EmoteSetChangeContext)}
- {@html colorizeName(ctx.actor)}
-
- {#if ctx.name}
- changed the active emote set to
- {ctx.name}.
- {:else}
- disabled the channel's emote set.
- {/if}
-{/snippet}
-
-{#snippet emoteSetUpdate(ctx: EmoteSetUpdateContext)}
- {@html colorizeName(ctx.actor)}
-
- {#if ctx.action === "renamed"}
- renamed {ctx.oldName} to
- {ctx.emote.name}
- {:else}
- {ctx.action} an emote:
- {ctx.emote.name}
- {/if}
-
-
-{/snippet}
-
-{#snippet mode(ctx: ModeContext)}
- {@html colorizeName(ctx.moderator)}
- {ctx.enabled ? "enabled" : "disabled"}
- {Number.isNaN(ctx.seconds) ? "" : formatDuration(ctx.seconds)}
- {ctx.mode === "slow" ? "slow mode." : `${ctx.mode} chat.`}
-{/snippet}
-
-{#snippet roleStatus(ctx: RoleStatusContext)}
- {@html colorizeName(ctx.moderator)}
- {ctx.added ? "added" : "removed"}
- {@html colorizeName(ctx.viewer)} as a {ctx.role}.
-{/snippet}
-
-{#snippet streamStatus(ctx: StreamStatusContext)}
- {@html colorizeName(message.channel.user)} is now {ctx.online ? "online" : "offline"}.
-{/snippet}
-
-{#snippet suspicionStatus(ctx: SuspicionStatusContext)}
- {@html colorizeName(ctx.moderator)}
- {ctx.active ? "started" : "stopped"}
- {monitored ? "monitoring" : restricted ? "restricting" : ctx.previous}
- {@html colorizeName(ctx.viewer)}'s messages.
-{/snippet}
-
-{#snippet term(ctx: TermContext)}
- {@const via = ctx.data.from_automod ? " (via AutoMod)" : ""}
-
- {@html colorizeName(ctx.moderator)}
- {ctx.data.action === "add" ? "added" : "removed"}
-
- {#if ctx.data.terms.length === 1}
- a {ctx.data.list} term{via}: {ctx.data.terms[0]}
- {:else}
- {ctx.data.terms.length} {ctx.data.list} terms{via}: {ctx.data.terms.join(", ")}
- {/if}
-{/snippet}
-
-{#snippet timeout(ctx: TimeoutContext)}
- {@const target = colorizeName(ctx.viewer)}
- {@const duration = formatDuration(ctx.seconds)}
-
- {#if ctx.moderator}
- {@html colorizeName(ctx.moderator)} timed out {@html target} for {duration}
- {:else}
- {@html target} has been timed out for {duration}
- {/if}{ctx.reason ? `: ${ctx.reason}` : "."}
-{/snippet}
-
-{#snippet unbanRequest(ctx: UnbanRequestContext)}
- {@const target = colorizeName(ctx.viewer)}
-
- {#if "status" in ctx.request}
- {#if !ctx.moderator}
- {@html target}'s unban request was {ctx.request.status}.
- {:else}
- {@html colorizeName(ctx.moderator)}
- {ctx.request.status}
- {@html target}'s unban request{ctx.request.resolution_text
- ? `: ${ctx.request.resolution_text}`
- : "."}
- {/if}
- {:else}
- {@html target} submitted an unban request: {ctx.request.text}
- {/if}
-{/snippet}
-
-{#snippet warn(ctx: WarnContext)}
- {@const reasons = [ctx.warning.reason, ...(ctx.warning.chat_rules_cited ?? [])]
- .filter((r) => r !== null)
- .join(", ")}
-
- {@html colorizeName(ctx.moderator)} warned {@html colorizeName(ctx.viewer)}: {reasons}
-{/snippet}
diff --git a/src/lib/components/message/events/AutoMod.svelte b/src/lib/components/message/events/AutoMod.svelte
new file mode 100644
index 00000000..78b90a85
--- /dev/null
+++ b/src/lib/components/message/events/AutoMod.svelte
@@ -0,0 +1,21 @@
+
+
+{#if status === "expired"}
+ {@html target}'s message expired and was not shown in chat.
+{:else}
+ {@html colorizeName(moderator)} {status} {@html target}'s message.
+{/if}
diff --git a/src/lib/components/message/events/BanStatus.svelte b/src/lib/components/message/events/BanStatus.svelte
new file mode 100644
index 00000000..785ecf42
--- /dev/null
+++ b/src/lib/components/message/events/BanStatus.svelte
@@ -0,0 +1,22 @@
+
+
+{#if moderator}
+ {@html colorizeName(moderator)} {action} {@html target}
+{:else}
+ {@html target} has been {action}
+{/if}{reason ? `: ${reason}` : "."}
diff --git a/src/lib/components/message/events/Banned.svelte b/src/lib/components/message/events/Banned.svelte
new file mode 100644
index 00000000..3a128a71
--- /dev/null
+++ b/src/lib/components/message/events/Banned.svelte
@@ -0,0 +1,15 @@
+
+
+You are permanently banned from {@html colorizeName(channel.user)} and cannot send messages. If you have
+been unbanned, try
+.
diff --git a/src/lib/components/message/events/BlockStatus.svelte b/src/lib/components/message/events/BlockStatus.svelte
new file mode 100644
index 00000000..5c11e369
--- /dev/null
+++ b/src/lib/components/message/events/BlockStatus.svelte
@@ -0,0 +1,14 @@
+
+
+{blocked ? "Blocked" : "Unblocked"}
+{@html colorizeName(user)}
diff --git a/src/lib/components/message/events/Clear.svelte b/src/lib/components/message/events/Clear.svelte
new file mode 100644
index 00000000..7e9183d4
--- /dev/null
+++ b/src/lib/components/message/events/Clear.svelte
@@ -0,0 +1,18 @@
+
+
+{#if moderator}
+ {@html colorizeName(moderator)} cleared the chat
+{:else}
+ The chat has been cleared
+{/if}
+
+for non-moderator viewers.
diff --git a/src/lib/components/message/events/Delete.svelte b/src/lib/components/message/events/Delete.svelte
new file mode 100644
index 00000000..b1eeff72
--- /dev/null
+++ b/src/lib/components/message/events/Delete.svelte
@@ -0,0 +1,21 @@
+
+
+{#if moderator}
+ {@html colorizeName(moderator)} deleted {@html target}'s message: {text}
+{:else}
+ {@html target}'s message was deleted: {text}
+{/if}
diff --git a/src/lib/components/message/events/EmoteSetChange.svelte b/src/lib/components/message/events/EmoteSetChange.svelte
new file mode 100644
index 00000000..b38cb738
--- /dev/null
+++ b/src/lib/components/message/events/EmoteSetChange.svelte
@@ -0,0 +1,20 @@
+
+
+{@html colorizeName(actor)}
+
+{#if name}
+ changed the active emote set to
+ {name}.
+{:else}
+ disabled the channel's emote set.
+{/if}
diff --git a/src/lib/components/message/events/EmoteSetUpdate.svelte b/src/lib/components/message/events/EmoteSetUpdate.svelte
new file mode 100644
index 00000000..39ff966a
--- /dev/null
+++ b/src/lib/components/message/events/EmoteSetUpdate.svelte
@@ -0,0 +1,27 @@
+
+
+{@html colorizeName(actor)}
+
+{#if action === "renamed"}
+ renamed {oldName} to
+ {emote.name}
+{:else}
+ {action} an emote:
+ {emote.name}
+{/if}
+
+
diff --git a/src/lib/components/message/events/Join.svelte b/src/lib/components/message/events/Join.svelte
new file mode 100644
index 00000000..ecaa5506
--- /dev/null
+++ b/src/lib/components/message/events/Join.svelte
@@ -0,0 +1,12 @@
+
+
+Joined {@html colorizeName(channel.user)}
diff --git a/src/lib/components/message/events/Mode.svelte b/src/lib/components/message/events/Mode.svelte
new file mode 100644
index 00000000..a836066b
--- /dev/null
+++ b/src/lib/components/message/events/Mode.svelte
@@ -0,0 +1,18 @@
+
+
+{@html colorizeName(moderator)}
+{enabled ? "enabled" : "disabled"}
+{Number.isNaN(seconds) ? "" : formatDuration(seconds)}
+{mode === "slow" ? "slow mode." : `${mode} chat.`}
diff --git a/src/lib/components/message/events/Notice.svelte b/src/lib/components/message/events/Notice.svelte
new file mode 100644
index 00000000..a59c1b90
--- /dev/null
+++ b/src/lib/components/message/events/Notice.svelte
@@ -0,0 +1,9 @@
+
+
+{text}
diff --git a/src/lib/components/message/events/Raid.svelte b/src/lib/components/message/events/Raid.svelte
new file mode 100644
index 00000000..606bde7b
--- /dev/null
+++ b/src/lib/components/message/events/Raid.svelte
@@ -0,0 +1,15 @@
+
+
+{@html colorizeName(moderator)} is raiding {@html colorizeName(user)} with {viewers} viewers.
diff --git a/src/lib/components/message/events/RoleStatus.svelte b/src/lib/components/message/events/RoleStatus.svelte
new file mode 100644
index 00000000..18c6c4a3
--- /dev/null
+++ b/src/lib/components/message/events/RoleStatus.svelte
@@ -0,0 +1,17 @@
+
+
+{@html colorizeName(moderator)}
+{added ? "added" : "removed"}
+{@html colorizeName(viewer)} as a {role}.
diff --git a/src/lib/components/message/events/StreamStatus.svelte b/src/lib/components/message/events/StreamStatus.svelte
new file mode 100644
index 00000000..db7e8775
--- /dev/null
+++ b/src/lib/components/message/events/StreamStatus.svelte
@@ -0,0 +1,13 @@
+
+
+{@html colorizeName(channel.user)} is now {online ? "online" : "offline"}.
diff --git a/src/lib/components/message/events/SuspicionStatus.svelte b/src/lib/components/message/events/SuspicionStatus.svelte
new file mode 100644
index 00000000..a87dd74a
--- /dev/null
+++ b/src/lib/components/message/events/SuspicionStatus.svelte
@@ -0,0 +1,21 @@
+
+
+{@html colorizeName(moderator)}
+{active ? "started" : "stopped"}
+{monitored ? "monitoring" : restricted ? "restricting" : previous}
+{@html colorizeName(viewer)}'s messages.
diff --git a/src/lib/components/message/events/Term.svelte b/src/lib/components/message/events/Term.svelte
new file mode 100644
index 00000000..206ebad2
--- /dev/null
+++ b/src/lib/components/message/events/Term.svelte
@@ -0,0 +1,23 @@
+
+
+{@html colorizeName(moderator)}
+{data.action === "add" ? "added" : "removed"}
+
+{#if data.terms.length === 1}
+ a {data.list} term{via}: {data.terms[0]}
+{:else}
+ {data.terms.length} {data.list} terms{via}: {data.terms.join(", ")}
+{/if}
diff --git a/src/lib/components/message/events/Timeout.svelte b/src/lib/components/message/events/Timeout.svelte
new file mode 100644
index 00000000..59a01e81
--- /dev/null
+++ b/src/lib/components/message/events/Timeout.svelte
@@ -0,0 +1,22 @@
+
+
+{#if moderator}
+ {@html colorizeName(moderator)} timed out {@html target} for {duration}
+{:else}
+ {@html target} has been timed out for {duration}
+{/if}{reason ? `: ${reason}` : "."}
diff --git a/src/lib/components/message/events/UnbanRequest.svelte b/src/lib/components/message/events/UnbanRequest.svelte
new file mode 100644
index 00000000..39f44d7c
--- /dev/null
+++ b/src/lib/components/message/events/UnbanRequest.svelte
@@ -0,0 +1,32 @@
+
+
+{#if "status" in request}
+ {#if !moderator}
+ {@html target}'s unban request was {request.status}.
+ {:else}
+ {@html colorizeName(moderator)}
+ {request.status}
+ {@html target}'s unban request{request.resolution_text
+ ? `: ${request.resolution_text}`
+ : "."}
+ {/if}
+{:else}
+ {@html target} submitted an unban request: {request.text}
+{/if}
diff --git a/src/lib/components/message/events/Unraid.svelte b/src/lib/components/message/events/Unraid.svelte
new file mode 100644
index 00000000..38297c72
--- /dev/null
+++ b/src/lib/components/message/events/Unraid.svelte
@@ -0,0 +1,14 @@
+
+
+{@html colorizeName(moderator)} canceled the raid on {@html colorizeName(user)}.
diff --git a/src/lib/components/message/events/Untimeout.svelte b/src/lib/components/message/events/Untimeout.svelte
new file mode 100644
index 00000000..92a55632
--- /dev/null
+++ b/src/lib/components/message/events/Untimeout.svelte
@@ -0,0 +1,13 @@
+
+
+{@html colorizeName(moderator)} removed timeout on {@html colorizeName(viewer)}.
diff --git a/src/lib/components/message/events/Warn.svelte b/src/lib/components/message/events/Warn.svelte
new file mode 100644
index 00000000..65c0aba9
--- /dev/null
+++ b/src/lib/components/message/events/Warn.svelte
@@ -0,0 +1,19 @@
+
+
+{@html colorizeName(moderator)} warned {@html colorizeName(viewer)}: {reasons}
diff --git a/src/lib/components/message/events/WarnAck.svelte b/src/lib/components/message/events/WarnAck.svelte
new file mode 100644
index 00000000..0667e734
--- /dev/null
+++ b/src/lib/components/message/events/WarnAck.svelte
@@ -0,0 +1,12 @@
+
+
+{@html colorizeName(viewer)} acknowledged their warning.
diff --git a/src/lib/handlers/eventsub/automod-message-hold.ts b/src/lib/handlers/eventsub/automod-message-hold.ts
index baf728a2..108d89df 100644
--- a/src/lib/handlers/eventsub/automod-message-hold.ts
+++ b/src/lib/handlers/eventsub/automod-message-hold.ts
@@ -48,6 +48,6 @@ export default defineHandler({
boundaries,
};
- channel.chat.addMessage(message);
+ channel.chat.add(message);
},
});
diff --git a/src/lib/handlers/eventsub/automod-message-update.ts b/src/lib/handlers/eventsub/automod-message-update.ts
index 648b1693..12c8f850 100644
--- a/src/lib/handlers/eventsub/automod-message-update.ts
+++ b/src/lib/handlers/eventsub/automod-message-update.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import AutoMod from "$lib/components/message/events/AutoMod.svelte";
import type { UserMessage } from "$lib/models/message/user-message";
import { defineHandler } from "../helper";
@@ -17,11 +18,6 @@ export default defineHandler({
if (message) message.deleted = true;
- channel.chat.addSystemMessage({
- type: "autoMod",
- status: data.status,
- viewer,
- moderator,
- });
+ channel.chat.event(AutoMod, { status: data.status, viewer, moderator });
},
});
diff --git a/src/lib/handlers/eventsub/channel-chat-user-message-hold.ts b/src/lib/handlers/eventsub/channel-chat-user-message-hold.ts
index fd651cf4..602ac8cd 100644
--- a/src/lib/handlers/eventsub/channel-chat-user-message-hold.ts
+++ b/src/lib/handlers/eventsub/channel-chat-user-message-hold.ts
@@ -22,6 +22,6 @@ export default defineHandler({
boundaries: [],
};
- channel.chat.addMessage(message);
+ channel.chat.add(message);
},
});
diff --git a/src/lib/handlers/eventsub/channel-chat-user-message-update.ts b/src/lib/handlers/eventsub/channel-chat-user-message-update.ts
index b4db5dbf..cd20a798 100644
--- a/src/lib/handlers/eventsub/channel-chat-user-message-update.ts
+++ b/src/lib/handlers/eventsub/channel-chat-user-message-update.ts
@@ -14,6 +14,6 @@ export default defineHandler({
if (message) message.deleted = true;
- channel.chat.addSystemMessage(`A moderator ${data.status} your message.`);
+ channel.chat.notice(`A moderator ${data.status} your message.`);
},
});
diff --git a/src/lib/handlers/eventsub/channel-moderate.ts b/src/lib/handlers/eventsub/channel-moderate.ts
index 45cfe7ee..456bdde8 100644
--- a/src/lib/handlers/eventsub/channel-moderate.ts
+++ b/src/lib/handlers/eventsub/channel-moderate.ts
@@ -1,5 +1,15 @@
import { app } from "$lib/app.svelte";
-import { SystemMessage } from "$lib/models/message/system-message";
+import BanStatus from "$lib/components/message/events/BanStatus.svelte";
+import Clear from "$lib/components/message/events/Clear.svelte";
+import Delete from "$lib/components/message/events/Delete.svelte";
+import Mode from "$lib/components/message/events/Mode.svelte";
+import Raid from "$lib/components/message/events/Raid.svelte";
+import RoleStatus from "$lib/components/message/events/RoleStatus.svelte";
+import Term from "$lib/components/message/events/Term.svelte";
+import Timeout from "$lib/components/message/events/Timeout.svelte";
+import Unraid from "$lib/components/message/events/Unraid.svelte";
+import Untimeout from "$lib/components/message/events/Untimeout.svelte";
+import Warn from "$lib/components/message/events/Warn.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -8,7 +18,7 @@ export default defineHandler({
const channel = app.channels.get(data.broadcaster_user_id);
if (!channel) return;
- const message = new SystemMessage(channel);
+ const { chat } = channel;
const moderator = await channel.viewers.fetch(data.moderator_user_id);
switch (data.action) {
@@ -24,48 +34,45 @@ export default defineHandler({
? "unique-mode"
: "subscriber-only";
- message.context = {
- type: "mode",
+ chat.event(Mode, {
mode,
enabled: !data.action.includes("off"),
seconds: Number.NaN,
moderator,
- };
+ });
break;
}
case "followers":
case "followersoff": {
- message.context = {
- type: "mode",
+ chat.event(Mode, {
mode: "follower-only",
enabled: !data.action.includes("off"),
seconds: data.followers
? data.followers.follow_duration_minutes * 60
: Number.NaN,
moderator,
- };
+ });
break;
}
case "slow":
case "slowoff": {
- message.context = {
- type: "mode",
+ chat.event(Mode, {
mode: "slow",
enabled: data.slow !== null,
seconds: data.slow?.wait_time_seconds ?? Number.NaN,
moderator,
- };
+ });
break;
}
case "clear": {
- channel.chat.deleteMessages();
- message.context = { type: "clear", moderator };
+ chat.deleteMessages();
+ chat.event(Clear, { moderator });
break;
}
@@ -75,12 +82,11 @@ export default defineHandler({
const metadata = data.action === "delete" ? data.delete : data.shared_chat_delete;
const viewer = await channel.viewers.fetch(metadata.user_id);
- message.context = {
- type: "delete",
+ chat.event(Delete, {
text: metadata.message_body,
user: viewer.user,
moderator,
- };
+ });
break;
}
@@ -89,19 +95,14 @@ export default defineHandler({
case "add_permitted_term":
case "remove_blocked_term":
case "remove_permitted_term": {
- message.context = { type: "term", data: data.automod_terms, moderator };
+ chat.event(Term, { data: data.automod_terms, moderator });
break;
}
case "warn": {
const viewer = await channel.viewers.fetch(data.warn.user_id);
- message.context = {
- type: "warn",
- warning: data.warn,
- viewer,
- moderator,
- };
+ chat.event(Warn, { warning: data.warn, viewer, moderator });
break;
}
@@ -112,18 +113,17 @@ export default defineHandler({
data.action === "timeout" ? data.timeout : data.shared_chat_timeout;
const viewer = await channel.viewers.fetch(metadata.user_id);
- channel.chat.deleteMessages(metadata.user_id);
+ chat.deleteMessages(metadata.user_id);
const expiration = new Date(metadata.expires_at);
- const duration = expiration.getTime() - message.timestamp.getTime();
+ const duration = expiration.getTime() - Date.now();
- message.context = {
- type: "timeout",
+ chat.event(Timeout, {
seconds: Math.ceil(duration / 1000),
reason: metadata.reason,
viewer,
moderator,
- };
+ });
break;
}
@@ -134,11 +134,7 @@ export default defineHandler({
data.action === "untimeout" ? data.untimeout : data.shared_chat_untimeout;
const viewer = await channel.viewers.fetch(metadata.user_id);
- message.context = {
- type: "untimeout",
- viewer,
- moderator,
- };
+ chat.event(Untimeout, { viewer, moderator });
break;
}
@@ -149,16 +145,15 @@ export default defineHandler({
const viewer = await channel.viewers.fetch((isBan ? data.ban : data.unban).user_id);
if (isBan) {
- channel.chat.deleteMessages(data.ban.user_id);
+ chat.deleteMessages(data.ban.user_id);
}
- message.context = {
- type: "banStatus",
+ chat.event(BanStatus, {
banned: isBan,
reason: isBan ? data.ban.reason : null,
viewer,
moderator,
- };
+ });
break;
}
@@ -171,16 +166,15 @@ export default defineHandler({
);
if (isBan) {
- channel.chat.deleteMessages(data.shared_chat_ban.user_id);
+ chat.deleteMessages(data.shared_chat_ban.user_id);
}
- message.context = {
- type: "banStatus",
+ chat.event(BanStatus, {
banned: isBan,
reason: isBan ? data.shared_chat_ban.reason : null,
viewer,
moderator,
- };
+ });
break;
}
@@ -190,13 +184,7 @@ export default defineHandler({
const added = data.action === "mod";
const viewer = await channel.viewers.fetch((added ? data.mod : data.unmod).user_id);
- message.context = {
- type: "roleStatus",
- role: "moderator",
- added,
- viewer,
- moderator,
- };
+ chat.event(RoleStatus, { role: "moderator", added, viewer, moderator });
break;
}
@@ -206,13 +194,7 @@ export default defineHandler({
const added = data.action === "vip";
const viewer = await channel.viewers.fetch((added ? data.vip : data.unvip).user_id);
- message.context = {
- type: "roleStatus",
- role: "VIP",
- added,
- viewer,
- moderator,
- };
+ chat.event(RoleStatus, { role: "VIP", added, viewer, moderator });
break;
}
@@ -220,12 +202,11 @@ export default defineHandler({
case "raid": {
const viewer = await channel.viewers.fetch(data.raid.user_id);
- message.context = {
- type: "raid",
+ chat.event(Raid, {
viewers: data.raid.viewer_count,
user: viewer.user,
moderator,
- };
+ });
break;
}
@@ -233,11 +214,7 @@ export default defineHandler({
case "unraid": {
const viewer = await channel.viewers.fetch(data.unraid.user_id);
- message.context = {
- type: "unraid",
- user: viewer.user,
- moderator,
- };
+ chat.event(Unraid, { user: viewer.user, moderator });
break;
}
@@ -246,7 +223,5 @@ export default defineHandler({
return;
}
}
-
- channel.chat.addMessage(message);
},
});
diff --git a/src/lib/handlers/eventsub/channel-subscription-end.ts b/src/lib/handlers/eventsub/channel-subscription-end.ts
index 570dc83b..ade9d27b 100644
--- a/src/lib/handlers/eventsub/channel-subscription-end.ts
+++ b/src/lib/handlers/eventsub/channel-subscription-end.ts
@@ -10,6 +10,6 @@ export default defineHandler({
const tier = data.tier === "Prime" ? "Prime" : `Tier ${data.tier[0]}`;
const text = `Your ${data.is_gift ? "gifted" : ""} ${tier} subscription has ended.`;
- channel.chat.addSystemMessage(text);
+ channel.chat.notice(text);
},
});
diff --git a/src/lib/handlers/eventsub/channel-suspicious-user-message.ts b/src/lib/handlers/eventsub/channel-suspicious-user-message.ts
index 74791251..1a0ff240 100644
--- a/src/lib/handlers/eventsub/channel-suspicious-user-message.ts
+++ b/src/lib/handlers/eventsub/channel-suspicious-user-message.ts
@@ -20,6 +20,6 @@ export default defineHandler({
message.viewer.banEvasion = data.ban_evasion_evaluation;
}
- channel.chat.addMessage(message);
+ channel.chat.add(message);
},
});
diff --git a/src/lib/handlers/eventsub/channel-suspicious-user-update.ts b/src/lib/handlers/eventsub/channel-suspicious-user-update.ts
index 79dcf192..861a850f 100644
--- a/src/lib/handlers/eventsub/channel-suspicious-user-update.ts
+++ b/src/lib/handlers/eventsub/channel-suspicious-user-update.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import SuspicionStatus from "$lib/components/message/events/SuspicionStatus.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -23,8 +24,7 @@ export default defineHandler({
viewer.restricted = status === "restricted";
}
- channel.chat.addSystemMessage({
- type: "suspicionStatus",
+ channel.chat.event(SuspicionStatus, {
active: status !== "no_treatment",
previous: viewer.monitored ? "monitoring" : viewer.restricted ? "restricting" : null,
viewer,
diff --git a/src/lib/handlers/eventsub/channel-unban-request-create.ts b/src/lib/handlers/eventsub/channel-unban-request-create.ts
index 22b865ca..35a0c94f 100644
--- a/src/lib/handlers/eventsub/channel-unban-request-create.ts
+++ b/src/lib/handlers/eventsub/channel-unban-request-create.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import UnbanRequest from "$lib/components/message/events/UnbanRequest.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -9,10 +10,6 @@ export default defineHandler({
const viewer = await channel.viewers.fetch(data.user_id);
- channel.chat.addSystemMessage({
- type: "unbanRequest",
- request: data,
- viewer,
- });
+ channel.chat.event(UnbanRequest, { request: data, viewer });
},
});
diff --git a/src/lib/handlers/eventsub/channel-unban-request-resolve.ts b/src/lib/handlers/eventsub/channel-unban-request-resolve.ts
index 76e63400..774e1800 100644
--- a/src/lib/handlers/eventsub/channel-unban-request-resolve.ts
+++ b/src/lib/handlers/eventsub/channel-unban-request-resolve.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import UnbanRequest from "$lib/components/message/events/UnbanRequest.svelte";
import type { Viewer } from "$lib/models/viewer.svelte";
import { defineHandler } from "../helper";
@@ -16,11 +17,6 @@ export default defineHandler({
moderator = await channel.viewers.fetch(data.moderator_user_id);
}
- channel.chat.addSystemMessage({
- type: "unbanRequest",
- request: data,
- viewer,
- moderator,
- });
+ channel.chat.event(UnbanRequest, { request: data, viewer, moderator });
},
});
diff --git a/src/lib/handlers/eventsub/channel-warning-acknowledge.ts b/src/lib/handlers/eventsub/channel-warning-acknowledge.ts
index ecaba79d..54f1a259 100644
--- a/src/lib/handlers/eventsub/channel-warning-acknowledge.ts
+++ b/src/lib/handlers/eventsub/channel-warning-acknowledge.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import WarnAck from "$lib/components/message/events/WarnAck.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -9,9 +10,6 @@ export default defineHandler({
const viewer = await channel.viewers.fetch(data.user_id);
- channel.chat.addSystemMessage({
- type: "warnAck",
- viewer,
- });
+ channel.chat.event(WarnAck, { viewer });
},
});
diff --git a/src/lib/handlers/eventsub/stream-offline.ts b/src/lib/handlers/eventsub/stream-offline.ts
index 97f5f022..88d4a863 100644
--- a/src/lib/handlers/eventsub/stream-offline.ts
+++ b/src/lib/handlers/eventsub/stream-offline.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import StreamStatus from "$lib/components/message/events/StreamStatus.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -9,9 +10,6 @@ export default defineHandler({
channel.stream = null;
- channel.chat.addSystemMessage({
- type: "streamStatus",
- online: false,
- });
+ channel.chat.event(StreamStatus, { channel, online: false });
},
});
diff --git a/src/lib/handlers/eventsub/stream-online.ts b/src/lib/handlers/eventsub/stream-online.ts
index d039939d..b16be937 100644
--- a/src/lib/handlers/eventsub/stream-online.ts
+++ b/src/lib/handlers/eventsub/stream-online.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import StreamStatus from "$lib/components/message/events/StreamStatus.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -9,9 +10,6 @@ export default defineHandler({
channel.stream = await channel.fetchStream();
- channel.chat.addSystemMessage({
- type: "streamStatus",
- online: true,
- });
+ channel.chat.event(StreamStatus, { channel, online: true });
},
});
diff --git a/src/lib/handlers/irc/clearchat.ts b/src/lib/handlers/irc/clearchat.ts
index 8f9ade27..d8f885e0 100644
--- a/src/lib/handlers/irc/clearchat.ts
+++ b/src/lib/handlers/irc/clearchat.ts
@@ -1,5 +1,8 @@
import { app } from "$lib/app.svelte";
-import { SystemMessage } from "$lib/models/message/system-message";
+import Banned from "$lib/components/message/events/Banned.svelte";
+import BanStatus from "$lib/components/message/events/BanStatus.svelte";
+import Clear from "$lib/components/message/events/Clear.svelte";
+import Timeout from "$lib/components/message/events/Timeout.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -14,13 +17,9 @@ export default defineHandler({
return;
}
- const message = new SystemMessage(channel, data);
-
if (data.action.type === "clear") {
channel.chat.deleteMessages();
-
- message.context = { type: "clear" };
- channel.chat.addMessage(message);
+ channel.chat.event(Clear, {}, data);
return;
}
@@ -29,26 +28,18 @@ export default defineHandler({
channel.chat.deleteMessages(target.id);
if (data.action.type === "ban") {
- message.context = {
- type: "banStatus",
- banned: true,
- reason: null,
- viewer: target,
- };
-
if (!data.is_recent && target.id === app.user?.id) {
app.user.banned.add(channel.id);
- message.context = { type: "banned" };
+ channel.chat.event(Banned, { channel }, data);
+ } else {
+ channel.chat.event(BanStatus, { banned: true, reason: null, viewer: target }, data);
}
} else {
- message.context = {
- type: "timeout",
- seconds: data.action.duration.secs,
- reason: null,
- viewer: target,
- };
+ channel.chat.event(
+ Timeout,
+ { seconds: data.action.duration.secs, reason: null, viewer: target },
+ data,
+ );
}
-
- channel.chat.addMessage(message);
},
});
diff --git a/src/lib/handlers/irc/clearmsg.ts b/src/lib/handlers/irc/clearmsg.ts
index 452203b1..22370263 100644
--- a/src/lib/handlers/irc/clearmsg.ts
+++ b/src/lib/handlers/irc/clearmsg.ts
@@ -1,5 +1,5 @@
import { app } from "$lib/app.svelte";
-import { SystemMessage } from "$lib/models/message/system-message";
+import Delete from "$lib/components/message/events/Delete.svelte";
import type { UserMessage } from "$lib/models/message/user-message";
import { defineHandler } from "../helper";
@@ -18,15 +18,7 @@ export default defineHandler({
message.deleted = true;
if (data.is_recent || (!data.is_recent && app.user?.moderating.has(channel.id))) {
- const sysmsg = new SystemMessage(channel, data);
-
- sysmsg.context = {
- type: "delete",
- text: data.message_text,
- user: message.author,
- };
-
- channel.chat.addMessage(sysmsg);
+ channel.chat.event(Delete, { text: data.message_text, user: message.author }, data);
}
},
});
diff --git a/src/lib/handlers/irc/join.ts b/src/lib/handlers/irc/join.ts
index 934f67ab..d1f50b35 100644
--- a/src/lib/handlers/irc/join.ts
+++ b/src/lib/handlers/irc/join.ts
@@ -1,4 +1,5 @@
import { app } from "$lib/app.svelte";
+import Join from "$lib/components/message/events/Join.svelte";
import { log } from "$lib/log";
import { sendPresence } from "$lib/seventv";
import { defineHandler } from "../helper";
@@ -21,7 +22,7 @@ export default defineHandler({
viewer.broadcaster = true;
viewer.moderator = true;
- channel.chat.addSystemMessage({ type: "join" });
+ channel.chat.event(Join, { channel });
log.info(`Joined ${channel.user.displayName}`);
},
diff --git a/src/lib/handlers/irc/notice.ts b/src/lib/handlers/irc/notice.ts
index c0d9ea9a..a8bfd8ff 100644
--- a/src/lib/handlers/irc/notice.ts
+++ b/src/lib/handlers/irc/notice.ts
@@ -1,5 +1,5 @@
import { app } from "$lib/app.svelte";
-import { SystemMessage } from "$lib/models/message/system-message";
+import Banned from "$lib/components/message/events/Banned.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -12,11 +12,11 @@ export default defineHandler({
return;
}
- const message = new SystemMessage(channel, {
+ const meta = {
deleted: data.deleted,
is_recent: data.is_recent,
server_timestamp: data.recent_timestamp ?? Date.now(),
- });
+ };
switch (data.message_id) {
case "emote_only_on":
@@ -34,7 +34,7 @@ export default defineHandler({
.replace("This room", "The chat")
.replace("s-only", "-only");
- message.text = text;
+ channel.chat.notice(text, meta);
break;
}
@@ -43,11 +43,9 @@ export default defineHandler({
app.user.banned.add(channel.id);
}
- message.context = { type: "banned" };
+ channel.chat.event(Banned, { channel }, meta);
break;
}
}
-
- channel.chat.addMessage(message);
},
});
diff --git a/src/lib/handlers/irc/privmsg.ts b/src/lib/handlers/irc/privmsg.ts
index a78e1706..5a77eef4 100644
--- a/src/lib/handlers/irc/privmsg.ts
+++ b/src/lib/handlers/irc/privmsg.ts
@@ -27,6 +27,6 @@ export default defineHandler({
await message.setSource(data.source);
}
- channel.chat.addMessage(message);
+ channel.chat.add(message);
},
});
diff --git a/src/lib/handlers/irc/usernotice.ts b/src/lib/handlers/irc/usernotice.ts
index 8ef1ac8b..637b47b3 100644
--- a/src/lib/handlers/irc/usernotice.ts
+++ b/src/lib/handlers/irc/usernotice.ts
@@ -22,6 +22,6 @@ export default defineHandler({
await message.setSource(data.source);
}
- channel.chat.addMessage(message);
+ channel.chat.add(message);
},
});
diff --git a/src/lib/handlers/seventv/emote-set-update.ts b/src/lib/handlers/seventv/emote-set-update.ts
index 0cb6441a..488e41a8 100644
--- a/src/lib/handlers/seventv/emote-set-update.ts
+++ b/src/lib/handlers/seventv/emote-set-update.ts
@@ -1,10 +1,12 @@
import * as cache from "tauri-plugin-cache-api";
import { app } from "$lib/app.svelte";
+import EmoteSetUpdate from "$lib/components/message/events/EmoteSetUpdate.svelte";
import type { Emote } from "$lib/emotes";
-import { SystemMessage } from "$lib/models/message/system-message";
import type { EmoteChange } from "$lib/seventv";
import { defineHandler } from "../helper";
+type EmoteSetUpdateProps = import("svelte").ComponentProps;
+
function transform(emote: EmoteChange): Emote {
let width = 28;
let height = 28;
@@ -51,17 +53,12 @@ export default defineHandler({
if (!twitch) return;
const actor = await channel.viewers.fetch(twitch.id);
- const message = new SystemMessage(channel);
+ let last: EmoteSetUpdateProps | null = null;
for (const change of data.pushed ?? []) {
const emote = transform(change.value);
- message.context = {
- type: "emoteSetUpdate",
- action: "added",
- emote,
- actor,
- };
+ last = { action: "added", emote, actor };
channel.emotes.set(emote.name, emote);
}
@@ -70,12 +67,7 @@ export default defineHandler({
const emote = channel.emotes.get(change.old_value.name);
if (!emote) continue;
- message.context = {
- type: "emoteSetUpdate",
- action: "removed",
- emote,
- actor,
- };
+ last = { action: "removed", emote, actor };
channel.emotes.delete(change.old_value.name);
}
@@ -84,13 +76,7 @@ export default defineHandler({
const emote = channel.emotes.get(change.old_value.name);
if (!emote) continue;
- message.context = {
- type: "emoteSetUpdate",
- action: "renamed",
- oldName: emote.name,
- emote,
- actor,
- };
+ last = { action: "renamed", oldName: emote.name, emote, actor };
emote.name = change.value.name;
@@ -98,7 +84,7 @@ export default defineHandler({
channel.emotes.set(change.value.name, emote);
}
- channel.chat.addMessage(message);
+ if (last) channel.chat.event(EmoteSetUpdate, last);
await cache.remove(`emotes:${channel.id}`);
await cache.set(`emotes:${channel.id}`, channel.emotes.values().toArray());
diff --git a/src/lib/handlers/seventv/user-update.ts b/src/lib/handlers/seventv/user-update.ts
index 5f2c7504..e7a53ec6 100644
--- a/src/lib/handlers/seventv/user-update.ts
+++ b/src/lib/handlers/seventv/user-update.ts
@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import { app } from "$lib/app.svelte";
-import { SystemMessage } from "$lib/models/message/system-message";
+import EmoteSetChange from "$lib/components/message/events/EmoteSetChange.svelte";
import { defineHandler } from "../helper";
export default defineHandler({
@@ -9,8 +9,6 @@ export default defineHandler({
const channel = app.channels.values().find((c) => c.seventvId === data.id);
if (!channel) return;
- const message = new SystemMessage(channel);
-
const root = data.updated?.find((c) => c.key === "connections");
if (!root) return;
@@ -28,16 +26,9 @@ export default defineHandler({
channel.emotes.clear("7TV");
if (child.value == null) {
- message.context = {
- type: "emoteSetChange",
- actor,
- };
+ channel.chat.event(EmoteSetChange, { actor });
} else {
- message.context = {
- type: "emoteSetChange",
- name: child.value.name,
- actor,
- };
+ channel.chat.event(EmoteSetChange, { name: child.value.name, actor });
await channel.emotes.fetch7tv();
await invoke("resub_emote_set", {
@@ -45,7 +36,5 @@ export default defineHandler({
setId: channel.emoteSetId,
});
}
-
- channel.chat.addMessage(message);
},
});
diff --git a/src/lib/models/chat.svelte.ts b/src/lib/models/chat.svelte.ts
index 554b0cd9..d4447fa7 100644
--- a/src/lib/models/chat.svelte.ts
+++ b/src/lib/models/chat.svelte.ts
@@ -6,11 +6,11 @@ import { settings } from "$lib/settings";
import { sendPresence } from "$lib/seventv";
import type { SentMessage } from "$lib/twitch/api";
import { commands } from "../commands";
+import Notice from "../components/message/events/Notice.svelte";
import type { Channel } from "./channel.svelte";
import { ComponentMessage } from "./message/component-message";
-import type { MessageContext } from "./message/context";
+import { EventMessage, type EventMessageData } from "./message/event-message";
import type { Message } from "./message/message";
-import { SystemMessage } from "./message/system-message";
import { TextualMessage } from "./message/textual-message.svelte";
import type { UserMessage } from "./message/user-message";
import { Viewer } from "./viewer.svelte";
@@ -89,20 +89,12 @@ export class Chat {
this.addCommands(commands);
}
- public addComponent>(
- component: C,
- props: ComponentProps = {} as never,
- ) {
- this.messages.push(new ComponentMessage(component, props));
- return this;
- }
-
- public addMessage(message: TextualMessage) {
+ public add(message: Message) {
if (this.messages.some((m) => m.id === message.id)) {
return this;
}
- if (message.recent) {
+ if (message instanceof TextualMessage && message.recent) {
if (this.#lastRecentAt === null) {
this.messages.unshift(message);
this.#lastRecentAt = 0;
@@ -117,17 +109,29 @@ export class Chat {
return this;
}
- public addSystemMessage(content: string | MessageContext) {
- let message: SystemMessage;
+ /**
+ * Adds a channel event rendered by the given component.
+ */
+ public event>(
+ component: C,
+ props: ComponentProps,
+ data?: Partial,
+ ) {
+ return this.add(new EventMessage(this.channel, component, props, data));
+ }
- if (typeof content === "string") {
- message = new SystemMessage(this.channel, content);
- } else {
- message = new SystemMessage(this.channel);
- message.context = content;
- }
+ /**
+ * Adds a plain text system notice.
+ */
+ public notice(text: string, data?: Partial) {
+ return this.event(Notice, { text }, data);
+ }
- return this.addMessage(message);
+ /**
+ * Adds an arbitrary, chrome-less component to the chat.
+ */
+ public component>(component: C, props: ComponentProps) {
+ return this.add(new ComponentMessage(component, props));
}
public addCommands(commands: Command[]) {
@@ -297,7 +301,7 @@ export class Chat {
const reason = data.drop_reason.message;
log.warn(`Message dropped: ${reason}`);
- this.addSystemMessage(reason);
+ this.notice(reason);
}
}
diff --git a/src/lib/models/message/component-message.ts b/src/lib/models/message/component-message.ts
index e7ed86e5..de60d6bb 100644
--- a/src/lib/models/message/component-message.ts
+++ b/src/lib/models/message/component-message.ts
@@ -2,14 +2,21 @@ import type { Component, ComponentProps } from "svelte";
import { Message } from "./message";
export class ComponentMessage extends Message {
- public readonly [Symbol.toStringTag] = "ComponentMessage";
+ public override readonly [Symbol.toStringTag] = "ComponentMessage";
- public readonly id = crypto.randomUUID();
- public readonly timestamp = new Date();
+ public override readonly id = crypto.randomUUID();
+ public override readonly timestamp = new Date();
public constructor(
+ /**
+ * The component that renders the message.
+ */
public readonly component: C,
- public readonly props: ComponentProps = {} as never,
+
+ /**
+ * The props passed to the component.
+ */
+ public readonly props: ComponentProps,
) {
super();
}
diff --git a/src/lib/models/message/context.ts b/src/lib/models/message/context.ts
deleted file mode 100644
index 910db952..00000000
--- a/src/lib/models/message/context.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import type { Emote } from "$lib/emotes";
-import type {
- AutoModMessageStatus,
- AutoModTermsMetadata,
- ChannelUnbanRequestCreate,
- ChannelUnbanRequestResolve,
- WarnMetadata,
-} from "$lib/twitch/eventsub";
-import type { User } from "../user.svelte";
-import type { Viewer } from "../viewer.svelte";
-
-export interface AutoModContext {
- type: "autoMod";
- status: AutoModMessageStatus;
- viewer: Viewer;
- moderator: Viewer;
-}
-
-export interface BannedContext {
- type: "banned";
-}
-
-export interface BanStatusContext {
- type: "banStatus";
- banned: boolean;
- reason: string | null;
- viewer: Viewer;
- moderator?: Viewer;
-}
-
-export interface BlockStatusContext {
- type: "blockStatus";
- blocked: boolean;
- user: User;
-}
-
-export interface ClearContext {
- type: "clear";
- moderator?: Viewer;
-}
-
-export interface DeleteContext {
- type: "delete";
- text: string;
- user: User;
- moderator?: Viewer;
- source?: Viewer | null;
-}
-
-export interface EmoteSetChangeContext {
- type: "emoteSetChange";
- name?: string;
- actor: Viewer;
-}
-
-export interface EmoteSetUpdateContext {
- type: "emoteSetUpdate";
- action: "added" | "removed" | "renamed";
- oldName?: string;
- emote: Emote;
- actor: Viewer;
-}
-
-export interface JoinContext {
- type: "join";
-}
-
-export interface ModeContext {
- type: "mode";
- mode: string;
- enabled: boolean;
- seconds: number;
- moderator: Viewer;
-}
-
-export interface RaidContext {
- type: "raid";
- viewers: number;
- user: User;
- moderator: Viewer;
-}
-
-export interface RoleStatusContext {
- type: "roleStatus";
- role: string;
- added: boolean;
- viewer: Viewer;
- moderator: Viewer;
-}
-
-export interface StreamStatusContext {
- type: "streamStatus";
- online: boolean;
-}
-
-export interface SuspicionStatusContext {
- type: "suspicionStatus";
- active: boolean;
- previous: "monitoring" | "restricting" | null;
- viewer: Viewer;
- moderator: Viewer;
-}
-
-export interface TermContext {
- type: "term";
- data: AutoModTermsMetadata;
- moderator: Viewer;
-}
-
-export interface TimeoutContext {
- type: "timeout";
- seconds: number;
- reason: string | null;
- viewer: Viewer;
- moderator?: Viewer;
-}
-
-export interface UnbanRequestContext {
- type: "unbanRequest";
- request: ChannelUnbanRequestCreate | ChannelUnbanRequestResolve;
- viewer: Viewer;
- moderator?: Viewer;
-}
-
-export interface UnraidContext {
- type: "unraid";
- user: User;
- moderator: Viewer;
-}
-
-export interface UntimeoutContext {
- type: "untimeout";
- viewer: Viewer;
- moderator: Viewer;
-}
-
-export interface WarnContext {
- type: "warn";
- warning: WarnMetadata;
- viewer: Viewer;
- moderator: Viewer;
-}
-
-export interface WarnAckContext {
- type: "warnAck";
- viewer: Viewer;
-}
-
-export type MessageContext =
- | AutoModContext
- | BannedContext
- | BanStatusContext
- | BlockStatusContext
- | ClearContext
- | DeleteContext
- | EmoteSetChangeContext
- | EmoteSetUpdateContext
- | JoinContext
- | ModeContext
- | RaidContext
- | RoleStatusContext
- | StreamStatusContext
- | SuspicionStatusContext
- | TermContext
- | TimeoutContext
- | UnbanRequestContext
- | UnraidContext
- | UntimeoutContext
- | WarnContext
- | WarnAckContext;
diff --git a/src/lib/models/message/event-message.ts b/src/lib/models/message/event-message.ts
new file mode 100644
index 00000000..f0bf6c70
--- /dev/null
+++ b/src/lib/models/message/event-message.ts
@@ -0,0 +1,40 @@
+import type { Component, ComponentProps } from "svelte";
+import type { Channel } from "../channel.svelte";
+import { TextualMessage } from "./textual-message.svelte";
+
+export interface EventMessageData {
+ deleted: boolean;
+ is_recent: boolean;
+ server_timestamp: number;
+}
+
+/**
+ * Event messages are constructed internally to relay channel events.
+ */
+export class EventMessage extends TextualMessage {
+ public override readonly [Symbol.toStringTag] = "EventMessage";
+
+ public override readonly id = crypto.randomUUID();
+ public override text = "";
+
+ public constructor(
+ channel: Channel,
+
+ /**
+ * The component that renders the event.
+ */
+ public readonly component: C,
+
+ /**
+ * The props passed to the component.
+ */
+ public readonly props: ComponentProps,
+ data?: Partial,
+ ) {
+ super(channel, {
+ deleted: data?.deleted ?? false,
+ is_recent: data?.is_recent ?? false,
+ server_timestamp: data?.server_timestamp ?? Date.now(),
+ });
+ }
+}
diff --git a/src/lib/models/message/message.ts b/src/lib/models/message/message.ts
index 797e98d9..dfdb6f8e 100644
--- a/src/lib/models/message/message.ts
+++ b/src/lib/models/message/message.ts
@@ -1,5 +1,5 @@
import type { ComponentMessage } from "./component-message";
-import type { SystemMessage } from "./system-message";
+import type { EventMessage } from "./event-message";
import type { UserMessage } from "./user-message";
export abstract class Message {
@@ -19,8 +19,8 @@ export abstract class Message {
return this[Symbol.toStringTag] === "ComponentMessage";
}
- public isSystem(): this is SystemMessage {
- return this[Symbol.toStringTag] === "SystemMessage";
+ public isEvent(): this is EventMessage {
+ return this[Symbol.toStringTag] === "EventMessage";
}
public isUser(): this is UserMessage {
diff --git a/src/lib/models/message/system-message.ts b/src/lib/models/message/system-message.ts
deleted file mode 100644
index f49f0091..00000000
--- a/src/lib/models/message/system-message.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { Channel } from "../channel.svelte";
-import type { MessageContext } from "./context";
-import { TextualMessage } from "./textual-message.svelte";
-
-export interface SystemMessageData {
- deleted: boolean;
- is_recent: boolean;
- server_timestamp: number;
-}
-
-/**
- * System messages are messages constructed internally and sent to relay
- * information to the user.
- */
-export class SystemMessage extends TextualMessage {
- public override readonly id = crypto.randomUUID();
- public override text = "";
-
- public readonly [Symbol.toStringTag] = "SystemMessage";
-
- /**
- * The context associated with the message.
- */
- public context: MessageContext | null = null;
-
- public constructor(channel: Channel, data?: string | Partial) {
- const args = typeof data === "string" ? undefined : (data ?? {});
-
- super(channel, {
- deleted: args?.deleted ?? false,
- is_recent: args?.is_recent ?? false,
- server_timestamp: args?.server_timestamp ?? Date.now(),
- });
-
- if (typeof data === "string") {
- this.text = data;
- }
- }
-}
diff --git a/src/lib/models/message/textual-message.svelte.ts b/src/lib/models/message/textual-message.svelte.ts
index e7d0324e..8d1f139a 100644
--- a/src/lib/models/message/textual-message.svelte.ts
+++ b/src/lib/models/message/textual-message.svelte.ts
@@ -1,9 +1,9 @@
import type { BaseUserMessage } from "$lib/twitch/irc";
import type { Channel } from "../channel.svelte";
+import type { EventMessageData } from "./event-message";
import { Message } from "./message";
-import type { SystemMessageData } from "./system-message";
-export type MessageData = BaseUserMessage | SystemMessageData;
+export type MessageData = BaseUserMessage | EventMessageData;
export abstract class TextualMessage extends Message {
public abstract readonly id: string;
diff --git a/src/lib/models/message/user-message.ts b/src/lib/models/message/user-message.ts
index f809ac1b..6ebafb9d 100644
--- a/src/lib/models/message/user-message.ts
+++ b/src/lib/models/message/user-message.ts
@@ -44,11 +44,10 @@ function createPartialUser(channel: Channel, sender: BasicUser, color: string) {
export class UserMessage extends TextualMessage {
#nodes: Node[] = [];
+ public override readonly [Symbol.toStringTag] = "UserMessage";
public override readonly id: string;
public override readonly text: string;
- public readonly [Symbol.toStringTag] = "UserMessage";
-
/**
* The user who sent the message.
*/
@@ -118,7 +117,7 @@ export class UserMessage extends TextualMessage {
// message_text should only be possibly null if it's a USERNOTICE, in
// which case we can assume system_message is present
- this.text = data.message_text ?? (data as UserNoticeMessage).system_message;
+ this.text = data.message_text ?? ("system_message" in data ? data.system_message : "");
this.author = viewer?.user ?? createPartialUser(channel, data.sender, data.name_color);
this.viewer = viewer ?? null;