feat(ws-client): add TypingStart/TypingStop protocol types and client handlers

ws-protocol.ts: TypingStartPayload/TypingStopPayload interfaces,
WsEventPayload union types.

room-ws-client.ts: onTypingStart/onTypingStop callbacks, sendTyping()
method, event dispatch for typing.start/typing_start.

editor/types.ts: special_here/special_channel MentionType + description
field on MentionItem.
This commit is contained in:
ZhenYi 2026-04-24 00:04:36 +08:00
parent 5776af18ca
commit 59640c6f44
3 changed files with 44 additions and 2 deletions

View File

@ -2,13 +2,14 @@
* Core types for the IM editor (mentions, files, emojis).
*/
export type MentionType = 'user' | 'channel' | 'ai' | 'command';
export type MentionType = 'user' | 'channel' | 'ai' | 'command' | 'special_here' | 'special_channel';
export interface MentionItem {
id: string;
label: string;
type: MentionType;
avatar?: string;
description?: string; // shown under label in suggestion dropdown
}
export interface FileData {

View File

@ -78,6 +78,8 @@ export interface RoomWsCallbacks {
onMessagePinned?: (payload: import('./ws-protocol').MessagePinnedPayload) => void;
onMessageUnpinned?: (payload: import('./ws-protocol').MessageUnpinnedPayload) => void;
onUserPresence?: (payload: UserPresencePayload) => void;
onTypingStart?: (payload: import('./ws-protocol').TypingStartPayload) => void;
onTypingStop?: (payload: import('./ws-protocol').TypingStopPayload) => void;
onStatusChange?: (status: RoomWsStatus) => void;
onError?: (error: Error) => void;
/** Called each time the client sends a heartbeat ping */
@ -961,6 +963,14 @@ export class RoomWsClient {
return url;
}
/** Send a typing_start / typing_stop event directly via WebSocket push (no response needed). */
sendTyping(roomId: string, action: 'start' | 'stop'): void {
if (this.ws && this.status === 'open') {
const event = { type: 'event', event: `typing_${action}`, room_id: roomId };
this.ws.send(JSON.stringify(event));
}
}
private handleMessage(rawText: string): void {
// Handle raw JSON pong before full parsing — resets heartbeat
if (rawText.trim() === '{"type":"pong"}') {
@ -1033,6 +1043,22 @@ export class RoomWsClient {
status: ((event.data as { status?: string })?.status ?? 'offline') as 'online' | 'away' | 'dnd' | 'offline',
});
break;
case 'typing.start':
case 'typing_start':
this.callbacks.onTypingStart?.({
room_id: event.room_id ?? '',
user_id: (event.data as { user_id?: string })?.user_id ?? '',
username: (event.data as { username?: string })?.username ?? '',
avatar_url: (event.data as { avatar_url?: string })?.avatar_url,
});
break;
case 'typing.stop':
case 'typing_stop':
this.callbacks.onTypingStop?.({
room_id: event.room_id ?? '',
user_id: (event.data as { user_id?: string })?.user_id ?? '',
});
break;
default:
// Unknown event type - ignore silently
break;

View File

@ -133,7 +133,20 @@ export type WsResponseData =
| NotificationListData
| MentionListData
| SubscribeData
| UserInfo[];
| UserInfo[]
| null;
export interface TypingStartPayload {
room_id: string;
user_id: string;
username: string;
avatar_url?: string;
}
export interface TypingStopPayload {
room_id: string;
user_id: string;
}
export interface WsEvent {
type: 'event';
@ -155,6 +168,8 @@ export type WsEventPayload =
| { type: 'message_pinned'; data: MessagePinnedPayload }
| { type: 'message_unpinned'; data: MessageUnpinnedPayload }
| { type: 'user_presence'; data: UserPresencePayload }
| { type: 'typing_start'; data: TypingStartPayload }
| { type: 'typing_stop'; data: TypingStopPayload }
| { type: string; data: unknown }; // catch-all for unknown events
export interface RoomMessagePayload {