mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): broadcast typing indicators for human users (#175)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ const overrides = new Map([
|
||||
["src/features/settings/ui/SettingsView.tsx", 600],
|
||||
["src/features/sidebar/ui/AppSidebar.tsx", 850], // channels + forums creation forms
|
||||
["src/features/tokens/ui/TokenSettingsCard.tsx", 800],
|
||||
["src/shared/api/relayClientSession.ts", 725], // durable websocket session manager with reconnect/replay/recovery state
|
||||
["src/shared/api/relayClientSession.ts", 740], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator
|
||||
["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions
|
||||
["src-tauri/src/commands/agents.rs", 845], // remote agent lifecycle routing (local + provider branches) + scope enforcement; rustfmt adds line breaks around long tuple/closure blocks
|
||||
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from "react";
|
||||
import { useChannelLinks } from "@/features/messages/lib/useChannelLinks";
|
||||
import type { ChannelSuggestion } from "@/features/messages/lib/useChannelLinks";
|
||||
import { useMentions } from "@/features/messages/lib/useMentions";
|
||||
import { useTypingBroadcast } from "@/features/messages/useTypingBroadcast";
|
||||
import {
|
||||
type BlobDescriptor,
|
||||
pickAndUploadMedia,
|
||||
@@ -63,6 +64,7 @@ export function MessageComposer({
|
||||
|
||||
const mentions = useMentions(channelId);
|
||||
const channelLinks = useChannelLinks();
|
||||
const notifyTyping = useTypingBroadcast(channelId);
|
||||
|
||||
const [uploadState, setUploadState] = React.useState<{
|
||||
status: "idle" | "uploading" | "error";
|
||||
@@ -371,11 +373,15 @@ export function MessageComposer({
|
||||
updateDraftSelection(event.target);
|
||||
mentions.updateMentionQuery(nextContent, cursorPos);
|
||||
channelLinks.updateChannelQuery(nextContent, cursorPos);
|
||||
if (nextContent.trim().length > 0) {
|
||||
notifyTyping();
|
||||
}
|
||||
},
|
||||
[
|
||||
updateDraftSelection,
|
||||
mentions.updateMentionQuery,
|
||||
channelLinks.updateChannelQuery,
|
||||
notifyTyping,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { relayClient } from "@/shared/api/relayClient";
|
||||
|
||||
const TYPING_SEND_INTERVAL_MS = 3_000;
|
||||
|
||||
/**
|
||||
* Publishes kind:20002 typing indicators for the current user,
|
||||
* throttled to at most once every 3 seconds per channel.
|
||||
*/
|
||||
export function useTypingBroadcast(channelId: string | null | undefined) {
|
||||
const lastSentRef = useRef(0);
|
||||
const lastChannelRef = useRef(channelId);
|
||||
const channelIdRef = useRef(channelId);
|
||||
channelIdRef.current = channelId;
|
||||
|
||||
const notifyTyping = useCallback(() => {
|
||||
const id = channelIdRef.current;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset throttle when channel changes.
|
||||
if (lastChannelRef.current !== id) {
|
||||
lastChannelRef.current = id;
|
||||
lastSentRef.current = 0;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastSentRef.current < TYPING_SEND_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastSentRef.current = now;
|
||||
relayClient.sendTypingIndicator(id).catch(() => {});
|
||||
}, []);
|
||||
|
||||
return notifyTyping;
|
||||
}
|
||||
@@ -125,6 +125,21 @@ export class RelayClient {
|
||||
);
|
||||
}
|
||||
|
||||
async sendTypingIndicator(channelId: string) {
|
||||
// Bail when disconnected — not worth triggering a reconnect for ephemeral typing events.
|
||||
if (this.wsId === null) {
|
||||
return;
|
||||
}
|
||||
const event = await signRelayEvent({
|
||||
kind: KIND_TYPING_INDICATOR,
|
||||
content: "",
|
||||
tags: [["h", channelId]],
|
||||
});
|
||||
|
||||
// Fire-and-forget: no need to wait for relay acknowledgement.
|
||||
void this.sendRaw(["EVENT", event]).catch(() => {});
|
||||
}
|
||||
|
||||
async subscribeToChannel(
|
||||
channelId: string,
|
||||
onEvent: (event: RelayEvent) => void,
|
||||
|
||||
Reference in New Issue
Block a user