From fc31e776c0d9f01ad424065f95dc9e55e467ed06 Mon Sep 17 00:00:00 2001 From: Tim Disney Date: Fri, 8 May 2026 16:54:03 -0700 Subject: [PATCH] fix typing indicators --- src/bot.ts | 12 ++++-------- src/typing.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 src/typing.ts diff --git a/src/bot.ts b/src/bot.ts index a02e039..79c5da7 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -9,6 +9,7 @@ import { createSharedDeps, describeModel } from "./agent.js"; import { LaneManager } from "./lanes.js"; import { PresenceIndicator, idlePresence } from "./presence.js"; import { Scheduler } from "./scheduler.js"; +import { TypingIndicator } from "./typing.js"; import { chunkForDiscord } from "./util.js"; const TOKEN = process.env.DISCORD_BOT_TOKEN; @@ -47,6 +48,7 @@ async function main() { }); const presence = new PresenceIndicator(client); + const typing = new TypingIndicator(client); let scheduler: Scheduler | null = null; const deps = await createSharedDeps({ onSchedulesChanged: () => scheduler?.scheduleReload(), @@ -78,13 +80,7 @@ async function main() { if (!content) return; const channel = msg.channel; - const sendTyping = () => { - if ("sendTyping" in channel) { - channel.sendTyping().catch(() => {}); - } - }; - sendTyping(); - const typingTimer = setInterval(sendTyping, 8000); + await typing.start(msg.channelId); try { const result = await presence.track( @@ -110,7 +106,7 @@ async function main() { channel.send("⚠️ error — check logs").catch(() => {}); } } finally { - clearInterval(typingTimer); + typing.stop(msg.channelId); } }); diff --git a/src/typing.ts b/src/typing.ts new file mode 100644 index 0000000..073c03c --- /dev/null +++ b/src/typing.ts @@ -0,0 +1,45 @@ +import type { Client } from "discord.js"; + +const TYPING_INTERVAL_MS = 8_000; + +interface ActiveTyping { + refs: number; + timer: ReturnType; +} + +export class TypingIndicator { + private active = new Map(); + + constructor(private client: Client) {} + + private async send(channelId: string): Promise { + const channel = await this.client.channels.fetch(channelId, { force: true }); + if (!channel || !("sendTyping" in channel)) return; + await channel.sendTyping().catch(() => {}); + } + + async start(channelId: string): Promise { + const existing = this.active.get(channelId); + if (existing) { + existing.refs += 1; + return; + } + + await this.send(channelId); + const timer = setInterval(() => { + void this.send(channelId); + }, TYPING_INTERVAL_MS); + this.active.set(channelId, { refs: 1, timer }); + } + + stop(channelId: string): void { + const active = this.active.get(channelId); + if (!active) return; + + active.refs -= 1; + if (active.refs > 0) return; + + clearInterval(active.timer); + this.active.delete(channelId); + } +} -- 2.51.2