@sendbird/uikit-chat-hooks 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/lib/commonjs/channel/useActiveGroupChannel.js +22 -0
  4. package/lib/commonjs/channel/useActiveGroupChannel.js.map +1 -0
  5. package/lib/commonjs/channel/useGroupChannelList.js +21 -0
  6. package/lib/commonjs/channel/useGroupChannelList.js.map +1 -0
  7. package/lib/commonjs/channel/useGroupChannelListWithCollection.js +148 -0
  8. package/lib/commonjs/channel/useGroupChannelListWithCollection.js.map +1 -0
  9. package/lib/commonjs/channel/useGroupChannelListWithQuery.js +130 -0
  10. package/lib/commonjs/channel/useGroupChannelListWithQuery.js.map +1 -0
  11. package/lib/commonjs/channel/useGroupChannelMessages/index.js +21 -0
  12. package/lib/commonjs/channel/useGroupChannelMessages/index.js.map +1 -0
  13. package/lib/commonjs/channel/useGroupChannelMessages/reducer.js +158 -0
  14. package/lib/commonjs/channel/useGroupChannelMessages/reducer.js.map +1 -0
  15. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js +305 -0
  16. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js.map +1 -0
  17. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js +249 -0
  18. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js.map +1 -0
  19. package/lib/commonjs/common/useAppFeatures.js +44 -0
  20. package/lib/commonjs/common/useAppFeatures.js.map +1 -0
  21. package/lib/commonjs/common/usePushTrigger.js +33 -0
  22. package/lib/commonjs/common/usePushTrigger.js.map +1 -0
  23. package/lib/commonjs/common/useTotalUnreadChannelCount.js +29 -0
  24. package/lib/commonjs/common/useTotalUnreadChannelCount.js.map +1 -0
  25. package/lib/commonjs/common/useTotalUnreadMessageCount.js +40 -0
  26. package/lib/commonjs/common/useTotalUnreadMessageCount.js.map +1 -0
  27. package/lib/commonjs/common/useUserList.js +110 -0
  28. package/lib/commonjs/common/useUserList.js.map +1 -0
  29. package/lib/commonjs/handler/useChannelHandler.js +29 -0
  30. package/lib/commonjs/handler/useChannelHandler.js.map +1 -0
  31. package/lib/commonjs/handler/useConnectionHandler.js +29 -0
  32. package/lib/commonjs/handler/useConnectionHandler.js.map +1 -0
  33. package/lib/commonjs/handler/useUserEventHandler.js +29 -0
  34. package/lib/commonjs/handler/useUserEventHandler.js.map +1 -0
  35. package/lib/commonjs/index.js +102 -0
  36. package/lib/commonjs/index.js.map +1 -0
  37. package/lib/commonjs/model/CustomQuery.js +28 -0
  38. package/lib/commonjs/model/CustomQuery.js.map +1 -0
  39. package/lib/commonjs/types.js +6 -0
  40. package/lib/commonjs/types.js.map +1 -0
  41. package/lib/module/channel/useActiveGroupChannel.js +12 -0
  42. package/lib/module/channel/useActiveGroupChannel.js.map +1 -0
  43. package/lib/module/channel/useGroupChannelList.js +10 -0
  44. package/lib/module/channel/useGroupChannelList.js.map +1 -0
  45. package/lib/module/channel/useGroupChannelListWithCollection.js +135 -0
  46. package/lib/module/channel/useGroupChannelListWithCollection.js.map +1 -0
  47. package/lib/module/channel/useGroupChannelListWithQuery.js +117 -0
  48. package/lib/module/channel/useGroupChannelListWithQuery.js.map +1 -0
  49. package/lib/module/channel/useGroupChannelMessages/index.js +10 -0
  50. package/lib/module/channel/useGroupChannelMessages/index.js.map +1 -0
  51. package/lib/module/channel/useGroupChannelMessages/reducer.js +148 -0
  52. package/lib/module/channel/useGroupChannelMessages/reducer.js.map +1 -0
  53. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js +289 -0
  54. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js.map +1 -0
  55. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js +234 -0
  56. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js.map +1 -0
  57. package/lib/module/common/useAppFeatures.js +33 -0
  58. package/lib/module/common/useAppFeatures.js.map +1 -0
  59. package/lib/module/common/usePushTrigger.js +22 -0
  60. package/lib/module/common/usePushTrigger.js.map +1 -0
  61. package/lib/module/common/useTotalUnreadChannelCount.js +17 -0
  62. package/lib/module/common/useTotalUnreadChannelCount.js.map +1 -0
  63. package/lib/module/common/useTotalUnreadMessageCount.js +28 -0
  64. package/lib/module/common/useTotalUnreadMessageCount.js.map +1 -0
  65. package/lib/module/common/useUserList.js +100 -0
  66. package/lib/module/common/useUserList.js.map +1 -0
  67. package/lib/module/handler/useChannelHandler.js +17 -0
  68. package/lib/module/handler/useChannelHandler.js.map +1 -0
  69. package/lib/module/handler/useConnectionHandler.js +17 -0
  70. package/lib/module/handler/useConnectionHandler.js.map +1 -0
  71. package/lib/module/handler/useUserEventHandler.js +17 -0
  72. package/lib/module/handler/useUserEventHandler.js.map +1 -0
  73. package/lib/module/index.js +13 -0
  74. package/lib/module/index.js.map +1 -0
  75. package/lib/module/model/CustomQuery.js +19 -0
  76. package/lib/module/model/CustomQuery.js.map +1 -0
  77. package/lib/module/types.js +2 -0
  78. package/lib/module/types.js.map +1 -0
  79. package/lib/typescript/src/channel/useActiveGroupChannel.d.ts +5 -0
  80. package/lib/typescript/src/channel/useGroupChannelList.d.ts +2 -0
  81. package/lib/typescript/src/channel/useGroupChannelListWithCollection.d.ts +2 -0
  82. package/lib/typescript/src/channel/useGroupChannelListWithQuery.d.ts +2 -0
  83. package/lib/typescript/src/channel/useGroupChannelMessages/index.d.ts +2 -0
  84. package/lib/typescript/src/channel/useGroupChannelMessages/reducer.d.ts +15 -0
  85. package/lib/typescript/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.d.ts +2 -0
  86. package/lib/typescript/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.d.ts +2 -0
  87. package/lib/typescript/src/common/useAppFeatures.d.ts +7 -0
  88. package/lib/typescript/src/common/usePushTrigger.d.ts +7 -0
  89. package/lib/typescript/src/common/useTotalUnreadChannelCount.d.ts +6 -0
  90. package/lib/typescript/src/common/useTotalUnreadMessageCount.d.ts +8 -0
  91. package/lib/typescript/src/common/useUserList.d.ts +24 -0
  92. package/lib/typescript/src/handler/useChannelHandler.d.ts +4 -0
  93. package/lib/typescript/src/handler/useConnectionHandler.d.ts +4 -0
  94. package/lib/typescript/src/handler/useUserEventHandler.d.ts +4 -0
  95. package/lib/typescript/src/index.d.ts +13 -0
  96. package/lib/typescript/src/model/CustomQuery.d.ts +14 -0
  97. package/lib/typescript/src/types.d.ts +198 -0
  98. package/package.json +73 -0
  99. package/src/channel/useActiveGroupChannel.ts +13 -0
  100. package/src/channel/useGroupChannelList.ts +11 -0
  101. package/src/channel/useGroupChannelListWithCollection.ts +144 -0
  102. package/src/channel/useGroupChannelListWithQuery.ts +124 -0
  103. package/src/channel/useGroupChannelMessages/index.ts +11 -0
  104. package/src/channel/useGroupChannelMessages/reducer.ts +134 -0
  105. package/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts +315 -0
  106. package/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts +251 -0
  107. package/src/common/useAppFeatures.ts +28 -0
  108. package/src/common/usePushTrigger.ts +31 -0
  109. package/src/common/useTotalUnreadChannelCount.ts +26 -0
  110. package/src/common/useTotalUnreadMessageCount.ts +43 -0
  111. package/src/common/useUserList.ts +103 -0
  112. package/src/handler/useChannelHandler.ts +25 -0
  113. package/src/handler/useConnectionHandler.ts +25 -0
  114. package/src/handler/useUserEventHandler.ts +26 -0
  115. package/src/index.ts +26 -0
  116. package/src/model/CustomQuery.ts +20 -0
  117. package/src/types.ts +246 -0
@@ -0,0 +1,134 @@
1
+ import { useReducer } from 'react';
2
+
3
+ import type {
4
+ SendbirdBaseMessage,
5
+ SendbirdFileMessage,
6
+ SendbirdMessage,
7
+ SendbirdUserMessage,
8
+ } from '@sendbird/uikit-utils';
9
+ import { arrayToMapWithGetter, getMessageUniqId, isMyMessage, isNewMessage, useIIFE } from '@sendbird/uikit-utils';
10
+
11
+ import type { UseGroupChannelMessagesOptions } from '../../types';
12
+
13
+ type Action =
14
+ | {
15
+ type: 'update_loading' | 'update_refreshing';
16
+ value: { status: boolean };
17
+ }
18
+ | {
19
+ type: 'update_messages' | 'update_next_messages';
20
+ value: { messages: SendbirdBaseMessage[]; clearPrev: boolean; currentUserId?: string };
21
+ }
22
+ | {
23
+ type: 'delete_messages' | 'delete_next_messages';
24
+ value: { messageIds: number[]; reqIds: string[] };
25
+ };
26
+
27
+ type State = {
28
+ loading: boolean;
29
+ refreshing: boolean;
30
+ messageMap: Record<string, SendbirdMessage>;
31
+ nextMessageMap: Record<string, SendbirdMessage>;
32
+ };
33
+
34
+ const defaultReducer = ({ ...draft }: State, action: Action) => {
35
+ switch (action.type) {
36
+ case 'update_refreshing':
37
+ case 'update_loading': {
38
+ const key = action.type === 'update_loading' ? 'loading' : 'refreshing';
39
+ draft[key] = action.value.status;
40
+
41
+ return draft;
42
+ }
43
+ case 'update_messages':
44
+ case 'update_next_messages': {
45
+ const key = action.type === 'update_messages' ? 'messageMap' : 'nextMessageMap';
46
+ const messageMap = arrayToMapWithGetter(action.value.messages, getMessageUniqId);
47
+ if (action.value.clearPrev) {
48
+ draft[key] = messageMap;
49
+ } else {
50
+ draft[key] = { ...draft[key], ...messageMap };
51
+
52
+ // NOTE: Replace pending message to succeeded message
53
+ action.value.messages
54
+ .filter((m): m is SendbirdUserMessage | SendbirdFileMessage => {
55
+ return (
56
+ (m.isFileMessage() || m.isUserMessage()) &&
57
+ Boolean(draft[key][m.reqId]) &&
58
+ m.sendingStatus === 'succeeded' &&
59
+ isMyMessage(m, action.value.currentUserId)
60
+ );
61
+ })
62
+ .forEach((m) => delete draft[key][m.reqId]);
63
+ }
64
+
65
+ if (action.type === 'update_messages') {
66
+ draft['nextMessageMap'] = {};
67
+ }
68
+
69
+ return draft;
70
+ }
71
+ case 'delete_messages':
72
+ case 'delete_next_messages': {
73
+ const key = action.type === 'delete_messages' ? 'messageMap' : 'nextMessageMap';
74
+ draft[key] = { ...draft[key] };
75
+ action.value.messageIds.forEach((msgId) => delete draft[key][msgId]);
76
+ action.value.reqIds.forEach((reqId) => delete draft[key][reqId]);
77
+
78
+ return draft;
79
+ }
80
+ }
81
+ };
82
+
83
+ export const useGroupChannelMessagesReducer = (
84
+ userId?: string,
85
+ sortComparator?: UseGroupChannelMessagesOptions['sortComparator'],
86
+ ) => {
87
+ const [{ loading, refreshing, messageMap, nextMessageMap }, dispatch] = useReducer(defaultReducer, {
88
+ loading: true,
89
+ refreshing: false,
90
+ messageMap: {},
91
+ nextMessageMap: {},
92
+ });
93
+
94
+ const updateMessages = (messages: SendbirdBaseMessage[], clearPrev: boolean, currentUserId?: string) => {
95
+ dispatch({ type: 'update_messages', value: { messages, clearPrev, currentUserId } });
96
+ };
97
+ const deleteMessages = (messageIds: number[], reqIds: string[]) => {
98
+ dispatch({ type: 'delete_messages', value: { messageIds, reqIds } });
99
+ };
100
+ const updateNextMessages = (messages: SendbirdBaseMessage[], clearPrev: boolean, currentUserId?: string) => {
101
+ dispatch({ type: 'update_next_messages', value: { messages, clearPrev, currentUserId } });
102
+ };
103
+ const deleteNextMessages = (messageIds: number[], reqIds: string[]) => {
104
+ dispatch({ type: 'delete_next_messages', value: { messageIds, reqIds } });
105
+ };
106
+ const updateLoading = (status: boolean) => {
107
+ dispatch({ type: 'update_loading', value: { status } });
108
+ };
109
+ const updateRefreshing = (status: boolean) => {
110
+ dispatch({ type: 'update_refreshing', value: { status } });
111
+ };
112
+
113
+ const messages = useIIFE(() => {
114
+ if (sortComparator) return Object.values(messageMap).sort(sortComparator);
115
+ return Object.values(messageMap);
116
+ });
117
+ const nextMessages = Object.values(nextMessageMap);
118
+ const newMessagesFromMembers = nextMessages.filter((msg) => isNewMessage(msg, userId));
119
+
120
+ return {
121
+ updateLoading,
122
+ updateRefreshing,
123
+ updateMessages,
124
+ updateNextMessages,
125
+ deleteMessages,
126
+ deleteNextMessages,
127
+
128
+ loading,
129
+ refreshing,
130
+ messages,
131
+ nextMessages,
132
+ newMessagesFromMembers,
133
+ };
134
+ };
@@ -0,0 +1,315 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+
3
+ import type {
4
+ SendbirdChannel,
5
+ SendbirdChatSDK,
6
+ SendbirdFileMessage,
7
+ SendbirdGroupChannel,
8
+ SendbirdMessageCollection,
9
+ SendbirdUserMessage,
10
+ } from '@sendbird/uikit-utils';
11
+ import { Logger, NOOP, isDifferentChannel, useForceUpdate, useIsMountedRef } from '@sendbird/uikit-utils';
12
+
13
+ import { useAppFeatures } from '../../common/useAppFeatures';
14
+ import { useChannelHandler } from '../../handler/useChannelHandler';
15
+ import type { UseGroupChannelMessages, UseGroupChannelMessagesOptions } from '../../types';
16
+ import { useActiveGroupChannel } from '../useActiveGroupChannel';
17
+ import { useGroupChannelMessagesReducer } from './reducer';
18
+
19
+ const createMessageCollection = (
20
+ sdk: SendbirdChatSDK,
21
+ channel: SendbirdGroupChannel,
22
+ creator?: UseGroupChannelMessagesOptions['collectionCreator'],
23
+ ) => {
24
+ if (creator) return creator();
25
+ const collection = channel.createMessageCollection();
26
+ const filter = new sdk.MessageFilter();
27
+ return collection.setLimit(100).setFilter(filter).build();
28
+ };
29
+
30
+ const HOOK_NAME = 'useGroupChannelMessagesWithCollection';
31
+
32
+ // FIXME: MessageCollection event handler bug, initialize(run async addObserver) -> dispose -> removeObserver -> addObserver called
33
+ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (sdk, channel, userId, options) => {
34
+ const isMounted = useIsMountedRef();
35
+ const disposeManuallyAfterUnmounted = () => {
36
+ if (!isMounted.current && collectionRef.current) collectionRef.current.dispose();
37
+ };
38
+
39
+ const { deliveryReceiptEnabled } = useAppFeatures(sdk);
40
+
41
+ const collectionRef = useRef<SendbirdMessageCollection>();
42
+
43
+ // NOTE: We cannot determine the channel object of Sendbird SDK is stale or not, so force update af
44
+ const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
45
+ const forceUpdate = useForceUpdate();
46
+
47
+ const {
48
+ loading,
49
+ refreshing,
50
+ messages,
51
+ nextMessages,
52
+ newMessagesFromMembers,
53
+ updateMessages,
54
+ updateNextMessages,
55
+ deleteNextMessages,
56
+ deleteMessages,
57
+ updateLoading,
58
+ updateRefreshing,
59
+ } = useGroupChannelMessagesReducer(userId, options?.sortComparator);
60
+
61
+ const channelMarkAs = async () => {
62
+ try {
63
+ if (deliveryReceiptEnabled) sdk.markAsDelivered(activeChannel.url);
64
+ } catch (e) {
65
+ Logger.error(`[${HOOK_NAME}/channelMarkAs/Delivered]`, e);
66
+ }
67
+ try {
68
+ await sdk.markAsReadWithChannelUrls([activeChannel.url]);
69
+ } catch (e) {
70
+ Logger.error(`[${HOOK_NAME}/channelMarkAs/Read]`, e);
71
+ }
72
+ };
73
+
74
+ const updateChannel = (channel: SendbirdChannel) => {
75
+ if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
76
+ setActiveChannel(channel);
77
+ forceUpdate();
78
+ }
79
+ };
80
+
81
+ const init = useCallback(
82
+ async (uid?: string, callback?: () => void) => {
83
+ if (collectionRef.current) collectionRef.current?.dispose();
84
+
85
+ if (uid) {
86
+ collectionRef.current = createMessageCollection(sdk, activeChannel, options?.collectionCreator);
87
+ updateNextMessages([], true, sdk.currentUser.userId);
88
+ channelMarkAs();
89
+
90
+ collectionRef.current
91
+ .initialize(sdk.MessageCollection.MessageCollectionInitPolicy.CACHE_AND_REPLACE_BY_API)
92
+ .onCacheResult((err, messages) => {
93
+ if (err) sdk.isCacheEnabled && Logger.error(`[${HOOK_NAME}/onCacheResult]`, err);
94
+ else {
95
+ Logger.debug(`[${HOOK_NAME}/onCacheResult]`, 'message length:', messages.length);
96
+ updateMessages(messages, true, sdk.currentUser.userId);
97
+ updateMessages(collectionRef.current?.pendingMessages ?? [], false, sdk.currentUser.userId);
98
+ updateMessages(collectionRef.current?.failedMessages ?? [], false, sdk.currentUser.userId);
99
+ if (messages?.length) callback?.();
100
+ }
101
+ })
102
+ .onApiResult((err, messages) => {
103
+ if (err) Logger.error(`[${HOOK_NAME}/onApiResult]`, err);
104
+ else {
105
+ Logger.debug(`[${HOOK_NAME}/onApiResult]`, 'message length:', messages.length);
106
+ updateMessages(messages, true, sdk.currentUser.userId);
107
+ if (sdk.isCacheEnabled) {
108
+ updateMessages(collectionRef.current?.pendingMessages ?? [], false, sdk.currentUser.userId);
109
+ updateMessages(collectionRef.current?.failedMessages ?? [], false, sdk.currentUser.userId);
110
+ }
111
+ }
112
+ callback?.();
113
+ });
114
+
115
+ collectionRef.current.setMessageCollectionHandler({
116
+ onMessagesAdded(_, channel, messages) {
117
+ disposeManuallyAfterUnmounted();
118
+ channelMarkAs();
119
+ updateNextMessages(messages, false, sdk.currentUser.userId);
120
+ updateChannel(channel);
121
+ },
122
+ onMessagesUpdated(_, channel, messages) {
123
+ disposeManuallyAfterUnmounted();
124
+ updateMessages(messages, false, sdk.currentUser.userId);
125
+ updateChannel(channel);
126
+ },
127
+ onMessagesDeleted(_, channel, messages) {
128
+ disposeManuallyAfterUnmounted();
129
+ const msgIds = messages.map((m) => m.messageId);
130
+ const reqIds = messages
131
+ .filter((m): m is SendbirdUserMessage | SendbirdFileMessage => 'reqId' in m)
132
+ .map((m) => m.reqId);
133
+
134
+ deleteMessages(msgIds, reqIds);
135
+ deleteNextMessages(msgIds, reqIds);
136
+ updateChannel(channel);
137
+ },
138
+ onChannelDeleted() {
139
+ disposeManuallyAfterUnmounted();
140
+ options?.onChannelDeleted?.();
141
+ },
142
+ onChannelUpdated(_, channel) {
143
+ disposeManuallyAfterUnmounted();
144
+ updateChannel(channel);
145
+ },
146
+ onHugeGapDetected() {
147
+ disposeManuallyAfterUnmounted();
148
+ init(uid);
149
+ },
150
+ });
151
+ }
152
+ },
153
+ [sdk, activeChannel.url, options?.collectionCreator],
154
+ );
155
+
156
+ useChannelHandler(
157
+ sdk,
158
+ HOOK_NAME,
159
+ {
160
+ onUserBanned(channel, bannedUser) {
161
+ disposeManuallyAfterUnmounted();
162
+ if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
163
+ if (bannedUser.userId === sdk.currentUser.userId) {
164
+ options?.onChannelDeleted?.();
165
+ } else {
166
+ setActiveChannel(channel);
167
+ forceUpdate();
168
+ }
169
+ }
170
+ },
171
+ },
172
+ [sdk],
173
+ );
174
+
175
+ useEffect(() => {
176
+ // NOTE: Cache read is heavy synchronous task, and it prevents smooth ui transition
177
+ setTimeout(async () => {
178
+ updateLoading(true);
179
+ init(userId, () => updateLoading(false));
180
+ }, 0);
181
+ }, [init, userId]);
182
+
183
+ useEffect(() => {
184
+ return () => {
185
+ if (collectionRef.current) collectionRef.current?.dispose();
186
+ };
187
+ }, []);
188
+
189
+ const refresh: ReturnType<UseGroupChannelMessages>['refresh'] = useCallback(async () => {
190
+ updateRefreshing(true);
191
+ init(userId, () => updateRefreshing(false));
192
+ }, [init, userId]);
193
+
194
+ const prev: ReturnType<UseGroupChannelMessages>['prev'] = useCallback(async () => {
195
+ if (collectionRef.current && collectionRef.current?.hasPrevious) {
196
+ try {
197
+ const list = await collectionRef.current?.loadPrevious();
198
+ updateMessages(list, false, sdk.currentUser.userId);
199
+ } catch {}
200
+ }
201
+ }, []);
202
+
203
+ const next: ReturnType<UseGroupChannelMessages>['next'] = useCallback(async () => {
204
+ const list = [];
205
+ if (collectionRef.current && collectionRef.current?.hasNext) {
206
+ try {
207
+ const fetchedList = await collectionRef.current?.loadNext();
208
+ list.push(...fetchedList);
209
+ } catch {}
210
+ }
211
+ if (nextMessages.length > 0) {
212
+ list.push(...nextMessages);
213
+ }
214
+ if (list.length > 0) {
215
+ updateMessages(list, false, sdk.currentUser.userId);
216
+ updateNextMessages([], true, sdk.currentUser.userId);
217
+ }
218
+ }, [nextMessages.length]);
219
+
220
+ const sendUserMessage: ReturnType<UseGroupChannelMessages>['sendUserMessage'] = useCallback(
221
+ (params, onPending) => {
222
+ return new Promise((resolve, reject) => {
223
+ const pendingMessage = activeChannel.sendUserMessage(params, (sentMessage, error) => {
224
+ if (error) reject(error);
225
+ else {
226
+ updateMessages([sentMessage], false, sdk.currentUser.userId);
227
+ resolve(sentMessage);
228
+ }
229
+ });
230
+
231
+ onPending?.(pendingMessage);
232
+ updateMessages([pendingMessage], false, sdk.currentUser.userId);
233
+ });
234
+ },
235
+ [activeChannel],
236
+ );
237
+ const sendFileMessage: ReturnType<UseGroupChannelMessages>['sendFileMessage'] = useCallback(
238
+ (params, onPending) => {
239
+ return new Promise((resolve, reject) => {
240
+ const pendingMessage = activeChannel.sendFileMessage(params, (sentMessage, error) => {
241
+ if (error) reject(error);
242
+ else {
243
+ updateMessages([sentMessage], false, sdk.currentUser.userId);
244
+ resolve(sentMessage as SendbirdFileMessage);
245
+ }
246
+ });
247
+
248
+ updateMessages([pendingMessage], false, sdk.currentUser.userId);
249
+ onPending?.(pendingMessage);
250
+ });
251
+ },
252
+ [activeChannel],
253
+ );
254
+ const updateUserMessage: ReturnType<UseGroupChannelMessages>['updateUserMessage'] = useCallback(
255
+ async (messageId, params) => {
256
+ const updatedMessage = await activeChannel.updateUserMessage(messageId, params, NOOP);
257
+ updateMessages([updatedMessage], false, sdk.currentUser.userId);
258
+ return updatedMessage;
259
+ },
260
+ [activeChannel],
261
+ );
262
+ const updateFileMessage: ReturnType<UseGroupChannelMessages>['updateFileMessage'] = useCallback(
263
+ async (messageId, params) => {
264
+ const updatedMessage = await activeChannel.updateFileMessage(messageId, params, NOOP);
265
+ updateMessages([updatedMessage], false, sdk.currentUser.userId);
266
+ return updatedMessage;
267
+ },
268
+ [activeChannel],
269
+ );
270
+ const resendMessage: ReturnType<UseGroupChannelMessages>['resendMessage'] = useCallback(
271
+ async (failedMessage) => {
272
+ const message = await (() => {
273
+ if (failedMessage.isUserMessage()) return activeChannel.resendUserMessage(failedMessage);
274
+ if (failedMessage.isFileMessage()) return activeChannel.resendFileMessage(failedMessage);
275
+ return null;
276
+ })();
277
+
278
+ if (message) updateMessages([message], false, sdk.currentUser.userId);
279
+ },
280
+ [activeChannel],
281
+ );
282
+ const deleteMessage: ReturnType<UseGroupChannelMessages>['deleteMessage'] = useCallback(
283
+ async (message) => {
284
+ if (message.sendingStatus === 'succeeded') {
285
+ if (message.isUserMessage()) await activeChannel.deleteMessage(message);
286
+ if (message.isFileMessage()) await activeChannel.deleteMessage(message);
287
+ } else {
288
+ try {
289
+ await collectionRef.current?.removeFailedMessages([message]);
290
+ } finally {
291
+ deleteMessages([message.messageId], [message.reqId]);
292
+ }
293
+ }
294
+ },
295
+ [activeChannel],
296
+ );
297
+
298
+ return {
299
+ loading,
300
+ refreshing,
301
+ refresh,
302
+ messages,
303
+ nextMessages,
304
+ newMessagesFromMembers,
305
+ next,
306
+ prev,
307
+ sendUserMessage,
308
+ sendFileMessage,
309
+ updateUserMessage,
310
+ updateFileMessage,
311
+ resendMessage,
312
+ deleteMessage,
313
+ activeChannel,
314
+ };
315
+ };
@@ -0,0 +1,251 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import type Sendbird from 'sendbird';
3
+
4
+ import type { SendbirdFileMessage, SendbirdGroupChannel, SendbirdOpenChannel } from '@sendbird/uikit-utils';
5
+ import { Logger, isDifferentChannel, useAsyncEffect, useForceUpdate } from '@sendbird/uikit-utils';
6
+
7
+ import { useAppFeatures } from '../../common/useAppFeatures';
8
+ import { useChannelHandler } from '../../handler/useChannelHandler';
9
+ import type { UseGroupChannelMessages, UseGroupChannelMessagesOptions } from '../../types';
10
+ import { useActiveGroupChannel } from '../useActiveGroupChannel';
11
+ import { useGroupChannelMessagesReducer } from './reducer';
12
+
13
+ const createMessageQuery = (
14
+ channel: SendbirdGroupChannel,
15
+ creator?: UseGroupChannelMessagesOptions['queryCreator'],
16
+ ) => {
17
+ if (creator) return creator();
18
+ const query = channel.createPreviousMessageListQuery();
19
+ query.limit = 100;
20
+ query.reverse = true;
21
+ return query;
22
+ };
23
+
24
+ const HOOK_NAME = 'useGroupChannelMessagesWithQuery';
25
+ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, channel, userId, options) => {
26
+ const { deliveryReceiptEnabled } = useAppFeatures(sdk);
27
+
28
+ const queryRef = useRef<Sendbird.PreviousMessageListQuery>();
29
+
30
+ // NOTE: We cannot determine the channel object of Sendbird SDK is stale or not, so force update after setActiveChannel
31
+ const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
32
+ const forceUpdate = useForceUpdate();
33
+
34
+ const {
35
+ loading,
36
+ refreshing,
37
+ messages,
38
+ nextMessages,
39
+ newMessagesFromMembers,
40
+ updateMessages,
41
+ updateNextMessages,
42
+ deleteNextMessages,
43
+ deleteMessages,
44
+ updateLoading,
45
+ updateRefreshing,
46
+ } = useGroupChannelMessagesReducer(userId, options?.sortComparator);
47
+
48
+ const channelMarkAs = async () => {
49
+ try {
50
+ if (deliveryReceiptEnabled) sdk.markAsDelivered(activeChannel.url);
51
+ } catch (e) {
52
+ Logger.error(`[${HOOK_NAME}/channelMarkAs/Delivered]`, e);
53
+ }
54
+ try {
55
+ await sdk.markAsReadWithChannelUrls([activeChannel.url]);
56
+ } catch (e) {
57
+ Logger.error(`[${HOOK_NAME}/channelMarkAs/Read]`, e);
58
+ }
59
+ };
60
+
61
+ const init = useCallback(
62
+ async (uid?: string) => {
63
+ if (uid) {
64
+ queryRef.current = createMessageQuery(activeChannel, options?.queryCreator);
65
+ channelMarkAs();
66
+ if (queryRef.current?.hasMore) {
67
+ const list = await queryRef.current?.load();
68
+ updateMessages(list, true, sdk.currentUser.userId);
69
+ }
70
+ updateNextMessages([], true, sdk.currentUser.userId);
71
+ }
72
+ },
73
+ [sdk, activeChannel.url, options?.queryCreator],
74
+ );
75
+
76
+ const channelUpdater = (channel: SendbirdGroupChannel | SendbirdOpenChannel) => {
77
+ if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
78
+ setActiveChannel(channel);
79
+ forceUpdate();
80
+ }
81
+ };
82
+
83
+ useChannelHandler(
84
+ sdk,
85
+ HOOK_NAME,
86
+ {
87
+ // Messages
88
+ onMessageReceived(eventChannel, message) {
89
+ if (isDifferentChannel(activeChannel, eventChannel)) return;
90
+ channelMarkAs();
91
+ updateNextMessages([message], false, sdk.currentUser.userId);
92
+ },
93
+ onMessageUpdated(eventChannel, message) {
94
+ if (isDifferentChannel(activeChannel, eventChannel)) return;
95
+ updateMessages([message], false, sdk.currentUser.userId);
96
+ },
97
+ onMessageDeleted(eventChannel, messageId) {
98
+ if (isDifferentChannel(activeChannel, eventChannel)) return;
99
+ deleteMessages([messageId], []);
100
+ deleteNextMessages([messageId], []);
101
+ },
102
+ // Channels
103
+ onChannelChanged: channelUpdater,
104
+ onChannelFrozen: channelUpdater,
105
+ onChannelUnfrozen: channelUpdater,
106
+ onChannelHidden: channelUpdater,
107
+ onChannelMemberCountChanged(channels) {
108
+ const channel = channels.find((c) => !isDifferentChannel(c, activeChannel));
109
+ if (channel) channelUpdater(channel);
110
+ },
111
+ onChannelDeleted(channelUrl: string) {
112
+ if (activeChannel.url === channelUrl) options?.onChannelDeleted?.();
113
+ },
114
+ // Users
115
+ onOperatorUpdated: channelUpdater,
116
+ onUserLeft: channelUpdater,
117
+ onUserEntered: channelUpdater,
118
+ onUserExited: channelUpdater,
119
+ onUserJoined: channelUpdater,
120
+ onUserUnbanned: channelUpdater,
121
+ onUserMuted: channelUpdater,
122
+ onUserUnmuted: channelUpdater,
123
+ onUserBanned(eventChannel, bannedUser) {
124
+ if (isDifferentChannel(activeChannel, eventChannel)) return;
125
+
126
+ if (bannedUser.userId === sdk.currentUser.userId) {
127
+ options?.onChannelDeleted?.();
128
+ } else {
129
+ channelUpdater(eventChannel);
130
+ }
131
+ },
132
+ },
133
+ [sdk],
134
+ );
135
+
136
+ useAsyncEffect(async () => {
137
+ updateLoading(true);
138
+ await init(userId);
139
+ updateLoading(false);
140
+ }, [init, userId]);
141
+
142
+ const refresh: ReturnType<UseGroupChannelMessages>['refresh'] = useCallback(async () => {
143
+ updateRefreshing(true);
144
+ await init(userId);
145
+ updateRefreshing(false);
146
+ }, [init, userId]);
147
+
148
+ const prev: ReturnType<UseGroupChannelMessages>['prev'] = useCallback(async () => {
149
+ if (queryRef.current && queryRef.current?.hasMore) {
150
+ const list = await queryRef.current?.load();
151
+ updateMessages(list, false, sdk.currentUser.userId);
152
+ }
153
+ }, []);
154
+
155
+ const next: ReturnType<UseGroupChannelMessages>['next'] = useCallback(async () => {
156
+ if (nextMessages.length > 0) {
157
+ updateMessages(nextMessages, false, sdk.currentUser.userId);
158
+ updateNextMessages([], true, sdk.currentUser.userId);
159
+ }
160
+ }, [nextMessages.length]);
161
+
162
+ const sendUserMessage: ReturnType<UseGroupChannelMessages>['sendUserMessage'] = useCallback(
163
+ (params, onPending) => {
164
+ return new Promise((resolve, reject) => {
165
+ const pendingMessage = activeChannel.sendUserMessage(params, (sentMessage, error) => {
166
+ if (error) reject(error);
167
+ else {
168
+ updateNextMessages([sentMessage], false, sdk.currentUser.userId);
169
+ resolve(sentMessage);
170
+ }
171
+ });
172
+ updateNextMessages([pendingMessage], false, sdk.currentUser.userId);
173
+ onPending?.(pendingMessage);
174
+ });
175
+ },
176
+ [activeChannel],
177
+ );
178
+ const sendFileMessage: ReturnType<UseGroupChannelMessages>['sendFileMessage'] = useCallback(
179
+ (params, onPending) => {
180
+ return new Promise((resolve, reject) => {
181
+ const pendingMessage = activeChannel.sendFileMessage(params, (sentMessage, error) => {
182
+ if (error) reject(error);
183
+ else {
184
+ updateNextMessages([sentMessage], false, sdk.currentUser.userId);
185
+ resolve(sentMessage as SendbirdFileMessage);
186
+ }
187
+ });
188
+ updateNextMessages([pendingMessage], false, sdk.currentUser.userId);
189
+ onPending?.(pendingMessage);
190
+ });
191
+ },
192
+ [activeChannel],
193
+ );
194
+ const updateUserMessage: ReturnType<UseGroupChannelMessages>['updateUserMessage'] = useCallback(
195
+ async (messageId, params) => {
196
+ const updatedMessage = await activeChannel.updateUserMessage(messageId, params);
197
+ updateMessages([updatedMessage], false, sdk.currentUser.userId);
198
+ return updatedMessage;
199
+ },
200
+ [activeChannel],
201
+ );
202
+ const updateFileMessage: ReturnType<UseGroupChannelMessages>['updateFileMessage'] = useCallback(
203
+ async (messageId, params) => {
204
+ const updatedMessage = await activeChannel.updateFileMessage(messageId, params);
205
+ updateMessages([updatedMessage], false, sdk.currentUser.userId);
206
+ return updatedMessage;
207
+ },
208
+ [activeChannel],
209
+ );
210
+ const resendMessage: ReturnType<UseGroupChannelMessages>['resendMessage'] = useCallback(
211
+ async (failedMessage) => {
212
+ const message = await (() => {
213
+ if (failedMessage.isUserMessage()) return activeChannel.resendUserMessage(failedMessage);
214
+ if (failedMessage.isFileMessage()) return activeChannel.resendFileMessage(failedMessage);
215
+ return null;
216
+ })();
217
+
218
+ if (message) updateNextMessages([message], false, sdk.currentUser.userId);
219
+ },
220
+ [activeChannel],
221
+ );
222
+ const deleteMessage: ReturnType<UseGroupChannelMessages>['deleteMessage'] = useCallback(
223
+ async (message) => {
224
+ if (message.sendingStatus === 'succeeded') {
225
+ if (message.isUserMessage()) await activeChannel.deleteMessage(message);
226
+ if (message.isFileMessage()) await activeChannel.deleteMessage(message);
227
+ } else {
228
+ deleteMessages([message.messageId], [message.reqId]);
229
+ }
230
+ },
231
+ [activeChannel],
232
+ );
233
+
234
+ return {
235
+ loading,
236
+ refreshing,
237
+ refresh,
238
+ messages,
239
+ nextMessages,
240
+ newMessagesFromMembers,
241
+ next,
242
+ prev,
243
+ sendUserMessage,
244
+ sendFileMessage,
245
+ updateUserMessage,
246
+ updateFileMessage,
247
+ resendMessage,
248
+ deleteMessage,
249
+ activeChannel,
250
+ };
251
+ };