@sendbird/uikit-chat-hooks 2.0.2 → 2.1.0

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 (48) hide show
  1. package/lib/commonjs/channel/useGroupChannel.js +33 -0
  2. package/lib/commonjs/channel/useGroupChannel.js.map +1 -0
  3. package/lib/commonjs/channel/useGroupChannelMessages/reducer.js +3 -3
  4. package/lib/commonjs/channel/useGroupChannelMessages/reducer.js.map +1 -1
  5. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js +56 -68
  6. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js.map +1 -1
  7. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js +26 -35
  8. package/lib/commonjs/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js.map +1 -1
  9. package/lib/commonjs/common/useAppFeatures.js +4 -4
  10. package/lib/commonjs/common/useAppFeatures.js.map +1 -1
  11. package/lib/commonjs/common/useMessageOutgoingStatus.js +1 -1
  12. package/lib/commonjs/common/useMessageOutgoingStatus.js.map +1 -1
  13. package/lib/commonjs/common/useUserList.js +25 -7
  14. package/lib/commonjs/common/useUserList.js.map +1 -1
  15. package/lib/commonjs/index.js +8 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/types.js.map +1 -1
  18. package/lib/module/channel/useGroupChannel.js +22 -0
  19. package/lib/module/channel/useGroupChannel.js.map +1 -0
  20. package/lib/module/channel/useGroupChannelMessages/reducer.js +3 -3
  21. package/lib/module/channel/useGroupChannelMessages/reducer.js.map +1 -1
  22. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js +57 -68
  23. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.js.map +1 -1
  24. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js +26 -34
  25. package/lib/module/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.js.map +1 -1
  26. package/lib/module/common/useAppFeatures.js +4 -4
  27. package/lib/module/common/useAppFeatures.js.map +1 -1
  28. package/lib/module/common/useMessageOutgoingStatus.js +1 -1
  29. package/lib/module/common/useMessageOutgoingStatus.js.map +1 -1
  30. package/lib/module/common/useUserList.js +27 -9
  31. package/lib/module/common/useUserList.js.map +1 -1
  32. package/lib/module/index.js +1 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/types.js.map +1 -1
  35. package/lib/typescript/src/channel/useGroupChannel.d.ts +6 -0
  36. package/lib/typescript/src/common/useUserList.d.ts +2 -2
  37. package/lib/typescript/src/index.d.ts +2 -1
  38. package/lib/typescript/src/types.d.ts +13 -6
  39. package/package.json +3 -3
  40. package/src/channel/useGroupChannel.ts +21 -0
  41. package/src/channel/useGroupChannelMessages/reducer.ts +4 -4
  42. package/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts +70 -76
  43. package/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts +29 -31
  44. package/src/common/useAppFeatures.ts +4 -4
  45. package/src/common/useMessageOutgoingStatus.ts +2 -1
  46. package/src/common/useUserList.ts +38 -12
  47. package/src/index.ts +3 -1
  48. package/src/types.ts +16 -7
@@ -2,16 +2,21 @@ import { useCallback, useEffect, useRef } from 'react';
2
2
 
3
3
  import { MessageCollectionInitPolicy, MessageEventSource, MessageFilter } from '@sendbird/chat/groupChannel';
4
4
  import type {
5
- SendbirdChannel,
5
+ SendbirdBaseMessage,
6
6
  SendbirdFileMessage,
7
7
  SendbirdGroupChannel,
8
8
  SendbirdMessageCollection,
9
9
  } from '@sendbird/uikit-utils';
10
- import { Logger, confirmAndMarkAsRead, isDifferentChannel, useForceUpdate } from '@sendbird/uikit-utils';
10
+ import {
11
+ Logger,
12
+ confirmAndMarkAsRead,
13
+ isDifferentChannel,
14
+ useForceUpdate,
15
+ useFreshCallback,
16
+ } from '@sendbird/uikit-utils';
11
17
 
12
18
  import { useChannelHandler } from '../../handler/useChannelHandler';
13
19
  import type { UseGroupChannelMessages, UseGroupChannelMessagesOptions } from '../../types';
14
- import { useActiveGroupChannel } from '../useActiveGroupChannel';
15
20
  import { useGroupChannelMessagesReducer } from './reducer';
16
21
 
17
22
  const createMessageCollection = (
@@ -26,11 +31,8 @@ const createMessageCollection = (
26
31
  const HOOK_NAME = 'useGroupChannelMessagesWithCollection';
27
32
 
28
33
  export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (sdk, channel, userId, options) => {
29
- const collectionRef = useRef<SendbirdMessageCollection>();
30
-
31
- // NOTE: We cannot determine the channel object of Sendbird SDK is stale or not, so force update af
32
- const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
33
34
  const forceUpdate = useForceUpdate();
35
+ const collectionRef = useRef<SendbirdMessageCollection>();
34
36
 
35
37
  const {
36
38
  loading,
@@ -48,24 +50,18 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
48
50
 
49
51
  const channelMarkAs = async () => {
50
52
  try {
51
- await confirmAndMarkAsRead([activeChannel]);
53
+ await confirmAndMarkAsRead([channel]);
52
54
  } catch (e) {
53
55
  Logger.warn(`[${HOOK_NAME}/channelMarkAs/Read]`, e);
54
56
  }
55
57
  };
56
58
 
57
- const updateChannel = (channel: SendbirdChannel) => {
58
- if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
59
- forceUpdate();
60
- }
61
- };
62
-
63
59
  const init = useCallback(
64
60
  async (uid?: string, callback?: () => void) => {
65
61
  if (collectionRef.current) collectionRef.current?.dispose();
66
62
 
67
63
  if (uid) {
68
- collectionRef.current = createMessageCollection(activeChannel, options?.collectionCreator);
64
+ collectionRef.current = createMessageCollection(channel, options?.collectionCreator);
69
65
  updateNextMessages([], true, sdk.currentUser.userId);
70
66
  channelMarkAs();
71
67
 
@@ -79,22 +75,25 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
79
75
  break;
80
76
  }
81
77
  updateNextMessages(messages, false, sdk.currentUser.userId);
82
- updateChannel(channel);
83
78
  },
84
- onMessagesUpdated: (_, channel, messages) => {
85
- updateMessages(messages, false, sdk.currentUser.userId);
86
- updateChannel(channel);
79
+ onMessagesUpdated: (_, __, messages) => {
80
+ // NOTE: v4 MESSAGE_RECEIVED is called twice from onMessagesAdded and onMessagesUpdated when receiving new message.
81
+ // This is not intended behavior but not bugs.
82
+ if (_.source !== MessageEventSource.EVENT_MESSAGE_RECEIVED) {
83
+ updateMessages(messages, false, sdk.currentUser.userId);
84
+ }
87
85
  },
88
- onMessagesDeleted: (_, channel, messageIds) => {
86
+ onMessagesDeleted: (_, __, messageIds) => {
89
87
  deleteMessages(messageIds, []);
90
88
  deleteNextMessages(messageIds, []);
91
- updateChannel(channel);
92
89
  },
93
90
  onChannelDeleted: () => {
94
91
  options?.onChannelDeleted?.();
95
92
  },
96
- onChannelUpdated: (_, channel) => {
97
- updateChannel(channel);
93
+ onChannelUpdated: (_, eventChannel) => {
94
+ if (eventChannel.isGroupChannel() && !isDifferentChannel(eventChannel, channel)) {
95
+ forceUpdate();
96
+ }
98
97
  },
99
98
  onHugeGapDetected: () => {
100
99
  init(uid);
@@ -107,16 +106,18 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
107
106
  if (err) sdk.isCacheEnabled && Logger.error(`[${HOOK_NAME}/onCacheResult]`, err);
108
107
  else {
109
108
  Logger.debug(`[${HOOK_NAME}/onCacheResult]`, 'message length:', messages.length);
109
+
110
110
  updateMessages(messages, true, sdk.currentUser.userId);
111
111
  updateMessages(collectionRef.current?.pendingMessages ?? [], false, sdk.currentUser.userId);
112
112
  updateMessages(collectionRef.current?.failedMessages ?? [], false, sdk.currentUser.userId);
113
- if (messages?.length) callback?.();
114
113
  }
114
+ callback?.();
115
115
  })
116
116
  .onApiResult((err, messages) => {
117
117
  if (err) Logger.warn(`[${HOOK_NAME}/onApiResult]`, err);
118
118
  else {
119
119
  Logger.debug(`[${HOOK_NAME}/onApiResult]`, 'message length:', messages.length);
120
+
120
121
  updateMessages(messages, true, sdk.currentUser.userId);
121
122
  if (sdk.isCacheEnabled) {
122
123
  updateMessages(collectionRef.current?.pendingMessages ?? [], false, sdk.currentUser.userId);
@@ -127,16 +128,15 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
127
128
  });
128
129
  }
129
130
  },
130
- [sdk, activeChannel.url, options?.collectionCreator],
131
+ [sdk, channel.url, options?.collectionCreator],
131
132
  );
132
133
 
133
134
  useChannelHandler(sdk, HOOK_NAME, {
134
135
  onUserBanned(channel, bannedUser) {
135
- if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
136
+ if (channel.isGroupChannel() && !isDifferentChannel(channel, channel)) {
136
137
  if (bannedUser.userId === sdk.currentUser.userId) {
137
138
  options?.onChannelDeleted?.();
138
139
  } else {
139
- setActiveChannel(channel);
140
140
  forceUpdate();
141
141
  }
142
142
  }
@@ -144,7 +144,7 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
144
144
  });
145
145
 
146
146
  useEffect(() => {
147
- // NOTE: Cache read is heavy synchronous task, and it prevents smooth ui transition
147
+ // NOTE: Cache read is heavy task, and it prevents smooth ui transition
148
148
  setTimeout(async () => {
149
149
  updateLoading(true);
150
150
  init(userId, () => updateLoading(false));
@@ -157,41 +157,44 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
157
157
  };
158
158
  }, []);
159
159
 
160
- const refresh: ReturnType<UseGroupChannelMessages>['refresh'] = useCallback(async () => {
160
+ const refresh: ReturnType<UseGroupChannelMessages>['refresh'] = useFreshCallback(async () => {
161
161
  updateRefreshing(true);
162
162
  init(userId, () => updateRefreshing(false));
163
- }, [init, userId]);
163
+ });
164
164
 
165
- const prev: ReturnType<UseGroupChannelMessages>['prev'] = useCallback(async () => {
165
+ const prev: ReturnType<UseGroupChannelMessages>['prev'] = useFreshCallback(async () => {
166
166
  if (collectionRef.current && collectionRef.current?.hasPrevious) {
167
167
  try {
168
168
  const list = await collectionRef.current?.loadPrevious();
169
169
  updateMessages(list, false, sdk.currentUser.userId);
170
170
  } catch {}
171
171
  }
172
- }, []);
172
+ });
173
+
174
+ const next: ReturnType<UseGroupChannelMessages>['next'] = useFreshCallback(async () => {
175
+ const messageCandidates: SendbirdBaseMessage[] = [];
173
176
 
174
- const next: ReturnType<UseGroupChannelMessages>['next'] = useCallback(async () => {
175
- const list = [];
176
177
  if (collectionRef.current && collectionRef.current?.hasNext) {
177
178
  try {
178
179
  const fetchedList = await collectionRef.current?.loadNext();
179
- list.push(...fetchedList);
180
+ messageCandidates.push(...fetchedList);
180
181
  } catch {}
181
182
  }
183
+
182
184
  if (nextMessages.length > 0) {
183
- list.push(...nextMessages);
185
+ messageCandidates.push(...nextMessages);
184
186
  }
185
- if (list.length > 0) {
186
- updateMessages(list, false, sdk.currentUser.userId);
187
+
188
+ if (messageCandidates.length > 0) {
189
+ updateMessages(messageCandidates, false, sdk.currentUser.userId);
187
190
  updateNextMessages([], true, sdk.currentUser.userId);
188
191
  }
189
- }, [nextMessages.length]);
192
+ });
190
193
 
191
- const sendUserMessage: ReturnType<UseGroupChannelMessages>['sendUserMessage'] = useCallback(
194
+ const sendUserMessage: ReturnType<UseGroupChannelMessages>['sendUserMessage'] = useFreshCallback(
192
195
  (params, onPending) => {
193
196
  return new Promise((resolve, reject) => {
194
- activeChannel
197
+ channel
195
198
  .sendUserMessage(params)
196
199
  .onPending((pendingMessage) => {
197
200
  if (pendingMessage.isUserMessage()) {
@@ -205,17 +208,17 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
205
208
  resolve(sentMessage);
206
209
  }
207
210
  })
208
- .onFailed((err) => {
211
+ .onFailed((err, _failedMessage) => {
212
+ // updateMessages([failedMessage], false, sdk.currentUser.userId);
209
213
  reject(err);
210
214
  });
211
215
  });
212
216
  },
213
- [activeChannel],
214
217
  );
215
- const sendFileMessage: ReturnType<UseGroupChannelMessages>['sendFileMessage'] = useCallback(
218
+ const sendFileMessage: ReturnType<UseGroupChannelMessages>['sendFileMessage'] = useFreshCallback(
216
219
  (params, onPending) => {
217
220
  return new Promise((resolve, reject) => {
218
- activeChannel
221
+ channel
219
222
  .sendFileMessage(params)
220
223
  .onPending((pendingMessage) => {
221
224
  if (pendingMessage.isFileMessage()) {
@@ -229,58 +232,50 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
229
232
  resolve(sentMessage as SendbirdFileMessage);
230
233
  }
231
234
  })
232
- .onFailed((err) => {
235
+ .onFailed((err, _failedMessage) => {
236
+ // updateMessages([failedMessage], false, sdk.currentUser.userId);
233
237
  reject(err);
234
238
  });
235
239
  });
236
240
  },
237
- [activeChannel],
238
241
  );
239
- const updateUserMessage: ReturnType<UseGroupChannelMessages>['updateUserMessage'] = useCallback(
242
+ const updateUserMessage: ReturnType<UseGroupChannelMessages>['updateUserMessage'] = useFreshCallback(
240
243
  async (messageId, params) => {
241
- const updatedMessage = await activeChannel.updateUserMessage(messageId, params);
244
+ const updatedMessage = await channel.updateUserMessage(messageId, params);
242
245
  updateMessages([updatedMessage], false, sdk.currentUser.userId);
243
246
  return updatedMessage;
244
247
  },
245
- [activeChannel],
246
248
  );
247
- const updateFileMessage: ReturnType<UseGroupChannelMessages>['updateFileMessage'] = useCallback(
249
+ const updateFileMessage: ReturnType<UseGroupChannelMessages>['updateFileMessage'] = useFreshCallback(
248
250
  async (messageId, params) => {
249
- const updatedMessage = await activeChannel.updateFileMessage(messageId, params);
251
+ const updatedMessage = await channel.updateFileMessage(messageId, params);
250
252
  updateMessages([updatedMessage], false, sdk.currentUser.userId);
251
253
  return updatedMessage;
252
254
  },
253
- [activeChannel],
254
255
  );
255
- const resendMessage: ReturnType<UseGroupChannelMessages>['resendMessage'] = useCallback(
256
+ const resendMessage: ReturnType<UseGroupChannelMessages>['resendMessage'] = useFreshCallback(
256
257
  async (failedMessage) => {
257
- const message = await (() => {
258
- if (failedMessage.isUserMessage()) return activeChannel.resendUserMessage(failedMessage);
259
- // FIXME: v4 bugs
260
- // @ts-ignore
261
- if (failedMessage.isFileMessage()) return activeChannel.resendFileMessage(failedMessage);
258
+ const resentMessage = await (() => {
259
+ if (failedMessage.isUserMessage()) return channel.resendUserMessage(failedMessage);
260
+ if (failedMessage.isFileMessage()) return channel.resendFileMessage(failedMessage);
262
261
  return null;
263
262
  })();
264
263
 
265
- if (message) updateMessages([message], false, sdk.currentUser.userId);
264
+ if (resentMessage) updateMessages([resentMessage], false, sdk.currentUser.userId);
266
265
  },
267
- [activeChannel],
268
266
  );
269
- const deleteMessage: ReturnType<UseGroupChannelMessages>['deleteMessage'] = useCallback(
270
- async (message) => {
271
- if (message.sendingStatus === 'succeeded') {
272
- if (message.isUserMessage()) await activeChannel.deleteMessage(message);
273
- if (message.isFileMessage()) await activeChannel.deleteMessage(message);
274
- } else {
275
- try {
276
- await collectionRef.current?.removeFailedMessage(message.reqId);
277
- } finally {
278
- deleteMessages([message.messageId], [message.reqId]);
279
- }
267
+ const deleteMessage: ReturnType<UseGroupChannelMessages>['deleteMessage'] = useFreshCallback(async (message) => {
268
+ if (message.sendingStatus === 'succeeded') {
269
+ if (message.isUserMessage()) await channel.deleteMessage(message);
270
+ if (message.isFileMessage()) await channel.deleteMessage(message);
271
+ } else {
272
+ try {
273
+ await collectionRef.current?.removeFailedMessage(message.reqId);
274
+ } finally {
275
+ deleteMessages([message.messageId], [message.reqId]);
280
276
  }
281
- },
282
- [activeChannel],
283
- );
277
+ }
278
+ });
284
279
 
285
280
  return {
286
281
  loading,
@@ -297,6 +292,5 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s
297
292
  updateFileMessage,
298
293
  resendMessage,
299
294
  deleteMessage,
300
- activeChannel,
301
295
  };
302
296
  };
@@ -1,6 +1,10 @@
1
1
  import { useCallback, useRef } from 'react';
2
2
 
3
- import type { SendbirdBaseChannel, SendbirdGroupChannel } from '@sendbird/uikit-utils';
3
+ import type {
4
+ SendbirdBaseChannel,
5
+ SendbirdGroupChannel,
6
+ SendbirdPreviousMessageListQuery,
7
+ } from '@sendbird/uikit-utils';
4
8
  import {
5
9
  Logger,
6
10
  confirmAndMarkAsRead,
@@ -8,11 +12,9 @@ import {
8
12
  useAsyncEffect,
9
13
  useForceUpdate,
10
14
  } from '@sendbird/uikit-utils';
11
- import type { SendbirdPreviousMessageListQuery } from '@sendbird/uikit-utils';
12
15
 
13
16
  import { useChannelHandler } from '../../handler/useChannelHandler';
14
17
  import type { UseGroupChannelMessages, UseGroupChannelMessagesOptions } from '../../types';
15
- import { useActiveGroupChannel } from '../useActiveGroupChannel';
16
18
  import { useGroupChannelMessagesReducer } from './reducer';
17
19
 
18
20
  const createMessageQuery = (
@@ -30,8 +32,6 @@ const HOOK_NAME = 'useGroupChannelMessagesWithQuery';
30
32
  export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, channel, userId, options) => {
31
33
  const queryRef = useRef<SendbirdPreviousMessageListQuery>();
32
34
 
33
- // NOTE: We cannot determine the channel object of Sendbird SDK is stale or not, so force update after setActiveChannel
34
- const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
35
35
  const forceUpdate = useForceUpdate();
36
36
 
37
37
  const {
@@ -59,7 +59,7 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
59
59
  const init = useCallback(
60
60
  async (uid?: string) => {
61
61
  if (uid) {
62
- queryRef.current = createMessageQuery(activeChannel, options?.queryCreator);
62
+ queryRef.current = createMessageQuery(channel, options?.queryCreator);
63
63
  channelMarkAs().catch();
64
64
  if (queryRef.current?.hasNext) {
65
65
  const list = await queryRef.current?.load();
@@ -68,12 +68,11 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
68
68
  updateNextMessages([], true, sdk.currentUser.userId);
69
69
  }
70
70
  },
71
- [sdk, activeChannel.url, options?.queryCreator],
71
+ [sdk, channel.url, options?.queryCreator],
72
72
  );
73
73
 
74
74
  const channelUpdater = (channel: SendbirdBaseChannel) => {
75
- if (channel.isGroupChannel() && !isDifferentChannel(channel, activeChannel)) {
76
- setActiveChannel(channel);
75
+ if (channel.isGroupChannel() && !isDifferentChannel(channel, channel)) {
77
76
  forceUpdate();
78
77
  }
79
78
  };
@@ -81,16 +80,16 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
81
80
  useChannelHandler(sdk, HOOK_NAME, {
82
81
  // Messages
83
82
  onMessageReceived(eventChannel, message) {
84
- if (isDifferentChannel(activeChannel, eventChannel)) return;
83
+ if (isDifferentChannel(channel, eventChannel)) return;
85
84
  channelMarkAs();
86
85
  updateNextMessages([message], false, sdk.currentUser.userId);
87
86
  },
88
87
  onMessageUpdated(eventChannel, message) {
89
- if (isDifferentChannel(activeChannel, eventChannel)) return;
88
+ if (isDifferentChannel(channel, eventChannel)) return;
90
89
  updateMessages([message], false, sdk.currentUser.userId);
91
90
  },
92
91
  onMessageDeleted(eventChannel, messageId) {
93
- if (isDifferentChannel(activeChannel, eventChannel)) return;
92
+ if (isDifferentChannel(channel, eventChannel)) return;
94
93
  deleteMessages([messageId], []);
95
94
  deleteNextMessages([messageId], []);
96
95
  },
@@ -100,11 +99,11 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
100
99
  onChannelUnfrozen: channelUpdater,
101
100
  onChannelHidden: channelUpdater,
102
101
  onChannelMemberCountChanged(channels) {
103
- const channel = channels.find((c) => !isDifferentChannel(c, activeChannel));
104
- if (channel) channelUpdater(channel);
102
+ const foundChannel = channels.find((c) => !isDifferentChannel(c, channel));
103
+ if (foundChannel) channelUpdater(foundChannel);
105
104
  },
106
105
  onChannelDeleted(channelUrl: string) {
107
- if (activeChannel.url === channelUrl) options?.onChannelDeleted?.();
106
+ if (channel.url === channelUrl) options?.onChannelDeleted?.();
108
107
  },
109
108
  // Users
110
109
  onOperatorUpdated: channelUpdater,
@@ -116,7 +115,7 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
116
115
  onUserMuted: channelUpdater,
117
116
  onUserUnmuted: channelUpdater,
118
117
  onUserBanned(eventChannel, bannedUser) {
119
- if (isDifferentChannel(activeChannel, eventChannel)) return;
118
+ if (isDifferentChannel(channel, eventChannel)) return;
120
119
 
121
120
  if (bannedUser.userId === sdk.currentUser.userId) {
122
121
  options?.onChannelDeleted?.();
@@ -155,7 +154,7 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
155
154
  const sendUserMessage: ReturnType<UseGroupChannelMessages>['sendUserMessage'] = useCallback(
156
155
  (params, onPending) => {
157
156
  return new Promise((resolve, reject) => {
158
- activeChannel
157
+ channel
159
158
  .sendUserMessage(params)
160
159
  .onPending((pendingMessage) => {
161
160
  if (pendingMessage.isUserMessage()) {
@@ -174,12 +173,12 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
174
173
  });
175
174
  });
176
175
  },
177
- [activeChannel],
176
+ [channel],
178
177
  );
179
178
  const sendFileMessage: ReturnType<UseGroupChannelMessages>['sendFileMessage'] = useCallback(
180
179
  (params, onPending) => {
181
180
  return new Promise((resolve, reject) => {
182
- activeChannel
181
+ channel
183
182
  .sendFileMessage(params)
184
183
  .onPending((pendingMessage) => {
185
184
  if (pendingMessage.isFileMessage()) {
@@ -198,48 +197,48 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
198
197
  });
199
198
  });
200
199
  },
201
- [activeChannel],
200
+ [channel],
202
201
  );
203
202
  const updateUserMessage: ReturnType<UseGroupChannelMessages>['updateUserMessage'] = useCallback(
204
203
  async (messageId, params) => {
205
- const updatedMessage = await activeChannel.updateUserMessage(messageId, params);
204
+ const updatedMessage = await channel.updateUserMessage(messageId, params);
206
205
  updateMessages([updatedMessage], false, sdk.currentUser.userId);
207
206
  return updatedMessage;
208
207
  },
209
- [activeChannel],
208
+ [channel],
210
209
  );
211
210
  const updateFileMessage: ReturnType<UseGroupChannelMessages>['updateFileMessage'] = useCallback(
212
211
  async (messageId, params) => {
213
- const updatedMessage = await activeChannel.updateFileMessage(messageId, params);
212
+ const updatedMessage = await channel.updateFileMessage(messageId, params);
214
213
  updateMessages([updatedMessage], false, sdk.currentUser.userId);
215
214
  return updatedMessage;
216
215
  },
217
- [activeChannel],
216
+ [channel],
218
217
  );
219
218
  const resendMessage: ReturnType<UseGroupChannelMessages>['resendMessage'] = useCallback(
220
219
  async (failedMessage) => {
221
220
  const message = await (() => {
222
- if (failedMessage.isUserMessage()) return activeChannel.resendUserMessage(failedMessage);
221
+ if (failedMessage.isUserMessage()) return channel.resendUserMessage(failedMessage);
223
222
  // FIXME: v4 bugs
224
223
  // @ts-ignore
225
- if (failedMessage.isFileMessage()) return activeChannel.resendFileMessage(failedMessage);
224
+ if (failedMessage.isFileMessage()) return channel.resendFileMessage(failedMessage);
226
225
  return null;
227
226
  })();
228
227
 
229
228
  if (message) updateNextMessages([message], false, sdk.currentUser.userId);
230
229
  },
231
- [activeChannel],
230
+ [channel],
232
231
  );
233
232
  const deleteMessage: ReturnType<UseGroupChannelMessages>['deleteMessage'] = useCallback(
234
233
  async (message) => {
235
234
  if (message.sendingStatus === 'succeeded') {
236
- if (message.isUserMessage()) await activeChannel.deleteMessage(message);
237
- if (message.isFileMessage()) await activeChannel.deleteMessage(message);
235
+ if (message.isUserMessage()) await channel.deleteMessage(message);
236
+ if (message.isFileMessage()) await channel.deleteMessage(message);
238
237
  } else {
239
238
  deleteMessages([message.messageId], [message.reqId]);
240
239
  }
241
240
  },
242
- [activeChannel],
241
+ [channel],
243
242
  );
244
243
 
245
244
  return {
@@ -257,6 +256,5 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c
257
256
  updateFileMessage,
258
257
  resendMessage,
259
258
  deleteMessage,
260
- activeChannel,
261
259
  };
262
260
  };
@@ -3,9 +3,9 @@ import { ApplicationAttributes, PremiumFeatures, SendbirdChatSDK } from '@sendbi
3
3
  export const useAppFeatures = (sdk: SendbirdChatSDK) => {
4
4
  const { premiumFeatureList = [], applicationAttributes = [] } = sdk.appInfo ?? {};
5
5
  return {
6
- deliveryReceiptEnabled: Boolean(premiumFeatureList.includes(PremiumFeatures.delivery_receipt)),
7
- broadcastChannelEnabled: Boolean(applicationAttributes.includes(ApplicationAttributes.allow_broadcast_channel)),
8
- superGroupChannelEnabled: Boolean(applicationAttributes.includes(ApplicationAttributes.allow_super_group_channel)),
9
- reactionEnabled: Boolean(applicationAttributes.includes(ApplicationAttributes.reactions)),
6
+ deliveryReceiptEnabled: premiumFeatureList.includes(PremiumFeatures.delivery_receipt),
7
+ broadcastChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_broadcast_channel),
8
+ superGroupChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_super_group_channel),
9
+ reactionEnabled: applicationAttributes.includes(ApplicationAttributes.reactions),
10
10
  };
11
11
  };
@@ -33,7 +33,6 @@ export const useMessageOutgoingStatus = (
33
33
  },
34
34
  });
35
35
 
36
- if (channel.isBroadcast || channel.isSuper) return 'NONE';
37
36
  if (!message) return 'NONE';
38
37
 
39
38
  if ('sendingStatus' in message) {
@@ -41,6 +40,8 @@ export const useMessageOutgoingStatus = (
41
40
  if (message.sendingStatus === 'failed') return 'FAILED';
42
41
  }
43
42
 
43
+ if (channel.isBroadcast || channel.isSuper) return 'NONE';
44
+
44
45
  if (channel.getUnreadMemberCount(message) === 0) return 'READ';
45
46
 
46
47
  if (features.deliveryReceiptEnabled) {
@@ -1,11 +1,14 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react';
1
+ import { useMemo, useRef, useState } from 'react';
2
2
 
3
3
  import type { Optional, SendbirdChatSDK, SendbirdUser } from '@sendbird/uikit-utils';
4
- import { Logger, SBErrorCode, SBErrorMessage, useAsyncEffect } from '@sendbird/uikit-utils';
4
+ import { Logger, SBErrorCode, SBErrorMessage, useAsyncEffect, useFreshCallback } from '@sendbird/uikit-utils';
5
5
 
6
- import type { CustomQueryInterface, UseUserListOptions, UseUserListReturn } from '../types';
6
+ import type { CustomQueryInterface, UseUserListOptions, UseUserListReturn, UserStruct } from '../types';
7
7
 
8
- const createUserQuery = <User>(sdk: SendbirdChatSDK, queryCreator?: UseUserListOptions<User>['queryCreator']) => {
8
+ const createUserQuery = <User extends UserStruct>(
9
+ sdk: SendbirdChatSDK,
10
+ queryCreator?: UseUserListOptions<User>['queryCreator'],
11
+ ) => {
9
12
  if (queryCreator) return queryCreator();
10
13
  // In order to use the API, the option must be turned on in the dashboard.
11
14
  return sdk.createApplicationUserListQuery() as unknown as CustomQueryInterface<User>;
@@ -34,7 +37,11 @@ const createUserQuery = <User>(sdk: SendbirdChatSDK, queryCreator?: UseUserListO
34
37
  * */
35
38
  export const useUserList = <
36
39
  Options extends UseUserListOptions<QueriedUser>,
37
- QueriedUser = Options['queryCreator'] extends Optional<() => CustomQueryInterface<infer User>> ? User : SendbirdUser,
40
+ QueriedUser extends UserStruct = Options['queryCreator'] extends Optional<
41
+ () => CustomQueryInterface<infer User extends UserStruct>
42
+ >
43
+ ? User
44
+ : SendbirdUser,
38
45
  >(
39
46
  sdk: SendbirdChatSDK,
40
47
  options?: Options,
@@ -51,12 +58,29 @@ export const useUserList = <
51
58
  return users;
52
59
  }, [users, options?.sortComparator]);
53
60
 
61
+ const upsertUser = useFreshCallback((user: QueriedUser) => {
62
+ setUsers(([...draft]) => {
63
+ const userIdx = draft.findIndex((it) => it.userId === user.userId);
64
+ if (userIdx > -1) draft[userIdx] = user;
65
+ else draft.push(user);
66
+ return draft;
67
+ });
68
+ });
69
+
70
+ const deleteUser = useFreshCallback((userId: QueriedUser['userId']) => {
71
+ setUsers(([...draft]) => {
72
+ const userIdx = draft.findIndex((it) => it.userId === userId);
73
+ if (userIdx > -1) draft.splice(userIdx, 1);
74
+ return draft;
75
+ });
76
+ });
77
+
54
78
  const updateUsers = (users: QueriedUser[], clearPrev: boolean) => {
55
79
  if (clearPrev) setUsers(users);
56
80
  else setUsers((prev) => prev.concat(users));
57
81
  };
58
82
 
59
- const init = useCallback(async () => {
83
+ const init = useFreshCallback(async () => {
60
84
  query.current = createUserQuery<QueriedUser>(sdk, options?.queryCreator);
61
85
  if (query.current?.hasNext) {
62
86
  const users = await query.current?.next().catch((err) => {
@@ -66,7 +90,7 @@ export const useUserList = <
66
90
  });
67
91
  updateUsers(users, true);
68
92
  }
69
- }, [sdk, options?.queryCreator]);
93
+ });
70
94
 
71
95
  useAsyncEffect(async () => {
72
96
  setLoading(true);
@@ -78,9 +102,9 @@ export const useUserList = <
78
102
  } finally {
79
103
  setLoading(false);
80
104
  }
81
- }, [init]);
105
+ }, []);
82
106
 
83
- const refresh = useCallback(async () => {
107
+ const refresh = useFreshCallback(async () => {
84
108
  setRefreshing(true);
85
109
  setError(null);
86
110
  try {
@@ -90,9 +114,9 @@ export const useUserList = <
90
114
  } finally {
91
115
  setRefreshing(false);
92
116
  }
93
- }, [init]);
117
+ });
94
118
 
95
- const next = useCallback(async () => {
119
+ const next = useFreshCallback(async () => {
96
120
  if (query.current && query.current?.hasNext) {
97
121
  const nextUsers = await query.current.next().catch((err) => {
98
122
  Logger.error(error);
@@ -101,12 +125,14 @@ export const useUserList = <
101
125
  });
102
126
  updateUsers(nextUsers, false);
103
127
  }
104
- }, []);
128
+ });
105
129
 
106
130
  return {
107
131
  loading,
108
132
  error,
109
133
  users: sortedUsers,
134
+ upsertUser,
135
+ deleteUser,
110
136
  next,
111
137
  refreshing,
112
138
  refresh,
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { useGroupChannelList } from './channel/useGroupChannelList';
2
2
  export { useGroupChannelMessages } from './channel/useGroupChannelMessages';
3
3
  export { useActiveGroupChannel } from './channel/useActiveGroupChannel';
4
+ export { useGroupChannel } from './channel/useGroupChannel';
4
5
 
5
6
  export { useAppFeatures } from './common/useAppFeatures';
6
7
  export { useMessageOutgoingStatus } from './common/useMessageOutgoingStatus';
@@ -19,8 +20,9 @@ export type {
19
20
  UseGroupChannelList,
20
21
  UseGroupChannelListOptions,
21
22
  UseGroupChannelMessages,
22
- UseUserListOptions,
23
23
  UseGroupChannelMessagesOptions,
24
+ UserStruct,
25
+ UseUserListOptions,
24
26
  UseUserListReturn,
25
27
  CustomQueryInterface,
26
28
  CustomBidirectionalQueryInterface,