-
Notifications
You must be signed in to change notification settings - Fork 2
/
use-typing.ts
196 lines (174 loc) · 6.91 KB
/
use-typing.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { ErrorCodes, errorInfoIs, RoomStatus, Typing, TypingEvent, TypingListener } from '@ably/chat';
import * as Ably from 'ably';
import { useCallback, useEffect, useState } from 'react';
import { wrapRoomPromise } from '../helper/room-promise.js';
import { useEventListenerRef } from '../helper/use-event-listener-ref.js';
import { useEventualRoomProperty } from '../helper/use-eventual-room.js';
import { useRoomContext } from '../helper/use-room-context.js';
import { useRoomStatus } from '../helper/use-room-status.js';
import { ChatStatusResponse } from '../types/chat-status-response.js';
import { Listenable } from '../types/listenable.js';
import { StatusParams } from '../types/status-params.js';
import { useChatConnection } from './use-chat-connection.js';
import { useLogger } from './use-logger.js';
/**
* The parameters for the {@link useTyping} hook.
*/
export interface TypingParams extends StatusParams, Listenable<TypingListener> {
/**
* A listener that will be called whenever a typing event is sent to the room.
* The listener is removed when the component unmounts.
*
*/
listener?: TypingListener;
}
export interface UseTypingResponse extends ChatStatusResponse {
/**
* A shortcut to the {@link Typing.start} method.
*/
readonly start: Typing['start'];
/**
* A shortcut to the {@link Typing.stop} method.
*/
readonly stop: Typing['stop'];
/**
* A state value representing the set of client IDs that are currently typing in the room.
* It automatically updates based on typing events received from the room.
*/
readonly currentlyTyping: TypingEvent['currentlyTyping'];
/**
* Provides access to the underlying {@link Typing} instance of the room.
*/
readonly typingIndicators?: Typing;
/**
* A state value representing the current error state of the hook, this will be an instance of {@link Ably.ErrorInfo} or `undefined`.
* An error can occur during mount when initially fetching the current typing state; this does not mean that further
* updates will not be received, and so the hook might recover from this state on its own.
*/
readonly error?: Ably.ErrorInfo;
}
/**
* A hook that provides access to the {@link Typing} instance in the room.
* It will use the instance belonging to the room in the nearest {@link ChatRoomProvider} in the component tree.
*
* @param params - Allows the registering of optional callbacks.
* @returns UseTypingResponse - An object containing the {@link Typing} instance and methods to interact with it.
*/
export const useTyping = (params?: TypingParams): UseTypingResponse => {
const { currentStatus: connectionStatus, error: connectionError } = useChatConnection({
onStatusChange: params?.onConnectionStatusChange,
});
const context = useRoomContext('useTyping');
const { status: roomStatus, error: roomError } = useRoomStatus(params);
const logger = useLogger();
logger.trace('useTyping();', { roomId: context.roomId });
const [currentlyTyping, setCurrentlyTyping] = useState<Set<string>>(new Set());
const [error, setError] = useState<Ably.ErrorInfo | undefined>();
// Create a stable reference for the listeners
const listenerRef = useEventListenerRef(params?.listener);
const onDiscontinuityRef = useEventListenerRef(params?.onDiscontinuity);
useEffect(() => {
// Start with a clean slate - no errors and empty set
setError(undefined);
setCurrentlyTyping((prev) => {
// keep reference constant if it's already empty
if (prev.size === 0) return prev;
return new Set<string>();
});
let mounted = true;
const setErrorState = (error?: Ably.ErrorInfo) => {
if (error === undefined) {
logger.debug('useTyping(); clearing error state', { roomId: context.roomId });
} else {
logger.error('useTyping(); setting error state', { error, roomId: context.roomId });
}
setError(error);
};
void context.room
.then((room) => {
// If we're not attached, we can't call typing.get() right now
if (room.status === RoomStatus.Attached) {
return room.typing
.get()
.then((currentlyTyping) => {
if (!mounted) return;
setCurrentlyTyping(currentlyTyping);
})
.catch((error: unknown) => {
const errorInfo = error as Ably.ErrorInfo;
if (!mounted || errorInfoIs(errorInfo, ErrorCodes.RoomIsReleased)) return;
setErrorState(errorInfo);
});
} else {
logger.debug('useTyping(); room not attached, setting currentlyTyping to empty', { roomId: context.roomId });
setCurrentlyTyping(new Set());
}
})
.catch();
return wrapRoomPromise(
context.room,
(room) => {
logger.debug('useTyping(); subscribing to typing events', { roomId: context.roomId });
const { unsubscribe } = room.typing.subscribe((event) => {
setErrorState(undefined);
setCurrentlyTyping(event.currentlyTyping);
});
return () => {
logger.debug('useTyping(); unsubscribing from typing events', { roomId: context.roomId });
mounted = false;
unsubscribe();
};
},
logger,
context.roomId,
).unmount();
}, [context, logger]);
// if provided, subscribes the user-provided onDiscontinuity listener
useEffect(() => {
if (!onDiscontinuityRef) return;
return wrapRoomPromise(
context.room,
(room) => {
logger.debug('useTyping(); applying onDiscontinuity listener', { roomId: context.roomId });
const { off } = room.typing.onDiscontinuity(onDiscontinuityRef);
return () => {
logger.debug('useTyping(); removing onDiscontinuity listener', { roomId: context.roomId });
off();
};
},
logger,
context.roomId,
).unmount();
}, [context, onDiscontinuityRef, logger]);
// if provided, subscribe the user-provided listener to TypingEvents
useEffect(() => {
if (!listenerRef) return;
return wrapRoomPromise(
context.room,
(room) => {
logger.debug('useTyping(); applying listener', { roomId: context.roomId });
const { unsubscribe } = room.typing.subscribe(listenerRef);
return () => {
logger.debug('useTyping(); removing listener', { roomId: context.roomId });
unsubscribe();
};
},
logger,
context.roomId,
).unmount();
}, [context, listenerRef, logger]);
// memoize the methods to avoid re-renders, and ensure the same instance is used
const start = useCallback(() => context.room.then((room) => room.typing.start()), [context]);
const stop = useCallback(() => context.room.then((room) => room.typing.stop()), [context]);
return {
typingIndicators: useEventualRoomProperty((room) => room.typing),
connectionStatus,
connectionError,
roomStatus,
roomError,
error,
start,
stop,
currentlyTyping,
};
};