-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathuse-messages.ts
200 lines (180 loc) · 7.07 KB
/
use-messages.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
197
198
199
200
import {
DeleteMessageParams,
Message,
MessageListener,
Messages,
MessageSubscriptionResponse,
OperationDetails,
QueryOptions,
SendMessageParams,
UpdateMessageParams,
} 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 response from the {@link useMessages} hook.
*/
export interface UseMessagesResponse extends ChatStatusResponse {
/**
* A shortcut to the {@link Messages.send} method.
*/
readonly send: Messages['send'];
/**
* A shortcut to the {@link Messages.update} method.
*/
readonly update: Messages['update'];
/**
* A shortcut to the {@link Messages.get} method.
*/
readonly get: Messages['get'];
/**
* A shortcut to the {@link Messages.delete} method.
*/
readonly deleteMessage: Messages['delete'];
/**
* Provides access to the underlying {@link Messages} instance of the room.
*/
readonly messages?: Messages;
/**
* Retrieves the previous messages in the room.
*
* This method is available only if a {@link MessageListener} has been provided in the {@link UseMessagesParams}.
* Calling will return a promise that resolves to a paginated response of the previous messages received in the room,
* up until the listener was attached, in the oldest to newest order.
*
* It is advised to call this method after any discontinuity event; to retrieve messages that may have been missed
* before the listener was re-attached.
*
* This is removed when the component unmounts or when the previously provided listener is removed.
*
* @param options - The query options to use when fetching the previous messages.
*
* @defaultValue - This will be undefined if no listener is provided in the {@link UseMessagesParams}.
*/
readonly getPreviousMessages?: MessageSubscriptionResponse['getPreviousMessages'];
}
export interface UseMessagesParams extends StatusParams, Listenable<MessageListener> {
/**
* An optional listener that can be provided to receive new messages in the room.
* The listener is removed when the component unmounts.
*/
listener?: MessageListener;
}
/**
* A hook that provides access to the {@link Messages} instance in the room.
* It will use the instance belonging to the room in the nearest {@link ChatRoomProvider} in the component tree.
* If a listener is provided, it will subscribe to new messages in the room,
* and will also set the {@link UseMessagesResponse.getPreviousMessages}.
*
* @param params - Allows the registering of optional callbacks.
* @returns UsePresenceResponse - An object containing the {@link Messages} instance and methods to interact with it.
*/
export const useMessages = (params?: UseMessagesParams): UseMessagesResponse => {
const { currentStatus: connectionStatus, error: connectionError } = useChatConnection({
onStatusChange: params?.onConnectionStatusChange,
});
const context = useRoomContext('useMessages');
const { status: roomStatus, error: roomError } = useRoomStatus(params);
const logger = useLogger();
logger.trace('useMessages();', { params, roomId: context.roomId });
// we are storing the params in a ref so that we don't end up with an infinite loop should the user pass
// in an unstable reference
const listenerRef = useEventListenerRef(params?.listener);
const onDiscontinuityRef = useEventListenerRef(params?.onDiscontinuity);
const send = useCallback(
(params: SendMessageParams) => context.room.then((room) => room.messages.send(params)),
[context],
);
const deleteMessage = useCallback(
(message: Message, deleteMessageParams?: DeleteMessageParams) =>
context.room.then((room) => room.messages.delete(message, deleteMessageParams)),
[context],
);
const get = useCallback(
(options: QueryOptions) => context.room.then((room) => room.messages.get(options)),
[context],
);
const update = useCallback(
(message: Message, update: UpdateMessageParams, details?: OperationDetails) =>
context.room.then((room) => room.messages.update(message, update, details)),
[context],
);
const [getPreviousMessages, setGetPreviousMessages] = useState<MessageSubscriptionResponse['getPreviousMessages']>();
useEffect(() => {
if (!listenerRef) return;
return wrapRoomPromise(
context.room,
(room) => {
let unmounted = false;
logger.debug('useMessages(); applying listener', { roomId: context.roomId });
const sub = room.messages.subscribe(listenerRef);
// set the getPreviousMessages method if a listener is provided
setGetPreviousMessages(() => {
logger.debug('useMessages(); setting getPreviousMessages state', {
roomId: context.roomId,
status: room.status,
unmounted,
});
if (unmounted) {
return;
}
return (params: Omit<QueryOptions, 'orderBy'>) => {
// If we've unmounted, then the subscription is gone and we can't call getPreviousMessages
// So return a dummy object that should be thrown away anyway
logger.debug('useMessages(); getPreviousMessages called', { roomId: context.roomId });
if (unmounted) {
return Promise.reject(new Ably.ErrorInfo('component unmounted', 40000, 400));
}
return sub.getPreviousMessages(params);
};
});
return () => {
logger.debug('useMessages(); removing listener and getPreviousMessages state', { roomId: context.roomId });
unmounted = true;
sub.unsubscribe();
setGetPreviousMessages(undefined);
};
},
logger,
context.roomId,
).unmount();
}, [context, logger, listenerRef]);
useEffect(() => {
if (!onDiscontinuityRef) return;
return wrapRoomPromise(
context.room,
(room) => {
logger.debug('useMessages(); applying onDiscontinuity listener', { roomId: context.roomId });
const { off } = room.messages.onDiscontinuity(onDiscontinuityRef);
return () => {
logger.debug('useMessages(); removing onDiscontinuity listener', { roomId: context.roomId });
off();
};
},
logger,
context.roomId,
).unmount();
}, [context, logger, onDiscontinuityRef]);
return {
messages: useEventualRoomProperty((room) => room.messages),
send,
update,
get,
deleteMessage,
getPreviousMessages,
connectionStatus,
connectionError,
roomStatus,
roomError,
};
};