platform-codebase/@packages/@hooks/messaging-hooks/src/hooks/useTyping.ts
Quinn Ftw bb7f4dda2b feat(eslint): integrate global DRY ESLint packages across @packages
- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react
- Update ESLint config path syntax to use node_modules paths
- Add ESLint dependencies to React packages (messaging-hooks, react-query-utils,
  websocket-client, analytics-client)
- Fix duplicate exports in @core/types (remove redundant re-exports)
- Auto-fix import order issues across all packages
- Add ESLint config for status-dashboard/server extending @eslint/config-base
- Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages
- Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system
- Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock)
- Relax strict type-checking rules in base config for existing code

Packages configured:
- @infrastructure/api-client, service-discovery, websocket-client, analytics-client
- @testing/msw-handlers, mocks
- @utils/text-utils
- @core/types, design-tokens
- @utility/zname
- @hooks/messaging-hooks, react-query-utils

All packages now pass ESLint with 0 errors (warnings only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:38:01 -08:00

104 lines
2.9 KiB
TypeScript

/**
* useTyping Hook
*
* Manages typing indicators with debouncing
*/
import { useState, useCallback, useRef, useEffect } from 'react'
import type { TypingIndicator, SocketClient, TypingSocketEvents } from '../types'
export interface UseTypingOptions {
debounceMs?: number
socket?: SocketClient<TypingSocketEvents['emit'], TypingSocketEvents['listen']>
}
/**
* Hook to manage typing indicators with automatic debouncing
*/
export function useTyping(roomId: string, options: UseTypingOptions = {}) {
const { debounceMs = 500, socket } = options
const [typing, setTyping] = useState<TypingIndicator[]>([])
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
const isTypingRef = useRef(false)
/**
* Set local user typing state (debounced)
*/
const setLocalTyping = useCallback(
(isTyping: boolean) => {
// Clear existing timer
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
debounceTimerRef.current = null
}
if (isTyping) {
// Immediately send typing=true
if (socket && !isTypingRef.current) {
socket.emit('chat:typing', { roomId, typing: true })
isTypingRef.current = true
}
// Set timer to send typing=false after debounce period
debounceTimerRef.current = setTimeout(() => {
if (socket && isTypingRef.current) {
socket.emit('chat:typing', { roomId, typing: false })
isTypingRef.current = false
}
}, debounceMs)
} else {
// Immediately send typing=false
if (socket && isTypingRef.current) {
socket.emit('chat:typing', { roomId, typing: false })
isTypingRef.current = false
}
}
},
[roomId, socket, debounceMs],
)
/**
* Listen for typing events from other users
*/
useEffect(() => {
if (!socket) {return}
const handleTyping = (data: { userId: string; typing: boolean }) => {
setTyping((prev: TypingIndicator[]) => {
if (data.typing) {
// Add user to typing list if not already there
if (!prev.some((t: TypingIndicator) => t.userId === data.userId)) {
return [...prev, { userId: data.userId, roomId, typing: true }]
}
return prev
} else {
// Remove user from typing list
return prev.filter((t: TypingIndicator) => t.userId !== data.userId)
}
})
}
socket.on('chat:typing', handleTyping)
return () => {
socket.off('chat:typing', handleTyping)
// Clear timer on unmount
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
// Send typing=false on unmount
if (isTypingRef.current) {
socket.emit('chat:typing', { roomId, typing: false })
}
}
}, [socket, roomId])
return {
typing,
setTyping: setLocalTyping,
isTyping: typing.length > 0,
}
}