@sendbird/uikit-react-native 3.2.0 → 3.3.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 (73) hide show
  1. package/lib/commonjs/components/ChannelInput/EditInput.js +2 -11
  2. package/lib/commonjs/components/ChannelInput/EditInput.js.map +1 -1
  3. package/lib/commonjs/components/ChannelInput/SendInput.js +2 -11
  4. package/lib/commonjs/components/ChannelInput/SendInput.js.map +1 -1
  5. package/lib/commonjs/components/ChannelInput/index.js +30 -3
  6. package/lib/commonjs/components/ChannelInput/index.js.map +1 -1
  7. package/lib/commonjs/components/ChannelMessageList/index.js +148 -116
  8. package/lib/commonjs/components/ChannelMessageList/index.js.map +1 -1
  9. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js +34 -1
  10. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js.map +1 -1
  11. package/lib/commonjs/domain/groupChannel/component/GroupChannelHeader.js +14 -4
  12. package/lib/commonjs/domain/groupChannel/component/GroupChannelHeader.js.map +1 -1
  13. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js +1 -0
  14. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  15. package/lib/commonjs/domain/groupChannel/types.js.map +1 -1
  16. package/lib/commonjs/fragments/createGroupChannelFragment.js +4 -3
  17. package/lib/commonjs/fragments/createGroupChannelFragment.js.map +1 -1
  18. package/lib/commonjs/index.js +4 -0
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/types.js +7 -0
  21. package/lib/commonjs/types.js.map +1 -1
  22. package/lib/commonjs/utils/promise.js +138 -0
  23. package/lib/commonjs/utils/promise.js.map +1 -0
  24. package/lib/commonjs/version.js +1 -1
  25. package/lib/commonjs/version.js.map +1 -1
  26. package/lib/module/components/ChannelInput/EditInput.js +3 -12
  27. package/lib/module/components/ChannelInput/EditInput.js.map +1 -1
  28. package/lib/module/components/ChannelInput/SendInput.js +3 -12
  29. package/lib/module/components/ChannelInput/SendInput.js.map +1 -1
  30. package/lib/module/components/ChannelInput/index.js +32 -5
  31. package/lib/module/components/ChannelInput/index.js.map +1 -1
  32. package/lib/module/components/ChannelMessageList/index.js +148 -116
  33. package/lib/module/components/ChannelMessageList/index.js.map +1 -1
  34. package/lib/module/components/GroupChannelMessageRenderer/index.js +34 -2
  35. package/lib/module/components/GroupChannelMessageRenderer/index.js.map +1 -1
  36. package/lib/module/domain/groupChannel/component/GroupChannelHeader.js +15 -5
  37. package/lib/module/domain/groupChannel/component/GroupChannelHeader.js.map +1 -1
  38. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js +1 -0
  39. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  40. package/lib/module/domain/groupChannel/types.js.map +1 -1
  41. package/lib/module/fragments/createGroupChannelFragment.js +4 -3
  42. package/lib/module/fragments/createGroupChannelFragment.js.map +1 -1
  43. package/lib/module/index.js +4 -0
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/types.js +5 -1
  46. package/lib/module/types.js.map +1 -1
  47. package/lib/module/utils/promise.js +132 -0
  48. package/lib/module/utils/promise.js.map +1 -0
  49. package/lib/module/version.js +1 -1
  50. package/lib/module/version.js.map +1 -1
  51. package/lib/typescript/src/components/ChannelInput/index.d.ts +2 -0
  52. package/lib/typescript/src/components/ChannelMessageList/index.d.ts +3 -0
  53. package/lib/typescript/src/components/GroupChannelMessageRenderer/index.d.ts +3 -0
  54. package/lib/typescript/src/components/OpenChannelMessageRenderer/index.d.ts +2 -0
  55. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  56. package/lib/typescript/src/domain/groupChannel/types.d.ts +3 -0
  57. package/lib/typescript/src/types.d.ts +4 -0
  58. package/lib/typescript/src/utils/promise.d.ts +7 -0
  59. package/lib/typescript/src/version.d.ts +1 -1
  60. package/package.json +6 -6
  61. package/src/components/ChannelInput/EditInput.tsx +3 -15
  62. package/src/components/ChannelInput/SendInput.tsx +2 -9
  63. package/src/components/ChannelInput/index.tsx +27 -4
  64. package/src/components/ChannelMessageList/index.tsx +144 -114
  65. package/src/components/GroupChannelMessageRenderer/index.tsx +34 -2
  66. package/src/domain/groupChannel/component/GroupChannelHeader.tsx +14 -3
  67. package/src/domain/groupChannel/component/GroupChannelMessageList.tsx +1 -0
  68. package/src/domain/groupChannel/types.ts +4 -0
  69. package/src/fragments/createGroupChannelFragment.tsx +11 -3
  70. package/src/index.ts +5 -1
  71. package/src/types.ts +5 -0
  72. package/src/utils/promise.ts +139 -0
  73. package/src/version.ts +1 -1
@@ -36,8 +36,9 @@ import type { CommonComponent } from '../../types';
36
36
  import ChatFlatList from '../ChatFlatList';
37
37
  import { ReactionAddons } from '../ReactionAddons';
38
38
 
39
- type PressActions = { onPress?: () => void; onLongPress?: () => void };
39
+ type PressActions = { onPress?: () => void; onLongPress?: () => void; bottomSheetItem?: BottomSheetItem };
40
40
  type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage;
41
+ type CreateMessagePressActions = (params: { message: SendbirdMessage }) => PressActions;
41
42
  export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpenChannel> = {
42
43
  enableMessageGrouping: boolean;
43
44
  currentUserId?: string;
@@ -74,6 +75,8 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
74
75
  channel: T;
75
76
  currentUserId?: ChannelMessageListProps<T>['currentUserId'];
76
77
  enableMessageGrouping: ChannelMessageListProps<T>['enableMessageGrouping'];
78
+ bottomSheetItem?: BottomSheetItem;
79
+ isFirstItem: boolean;
77
80
  }) => React.ReactElement | null;
78
81
  renderNewMessagesButton: null | CommonComponent<{
79
82
  visible: boolean;
@@ -121,7 +124,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
121
124
  const { colors } = useUIKitTheme();
122
125
  const { show } = useUserProfile();
123
126
  const { left, right } = useSafeAreaInsets();
124
- const getMessagePressActions = useGetMessagePressActions({
127
+ const createMessagePressActions = useCreateMessagePressActions({
125
128
  channel,
126
129
  currentUserId,
127
130
  onEditMessage,
@@ -134,7 +137,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
134
137
  const safeAreaLayout = { paddingLeft: left, paddingRight: right };
135
138
 
136
139
  const renderItem: ListRenderItem<SendbirdMessage> = useFreshCallback(({ item, index }) => {
137
- const { onPress, onLongPress } = getMessagePressActions(item);
140
+ const { onPress, onLongPress, bottomSheetItem } = createMessagePressActions({ message: item });
138
141
  return renderMessage({
139
142
  message: item,
140
143
  prevMessage: messages[index + 1],
@@ -147,6 +150,8 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
147
150
  channel,
148
151
  currentUserId,
149
152
  focused: (searchItem?.startingPoint ?? -1) === item.createdAt,
153
+ bottomSheetItem,
154
+ isFirstItem: index === 0,
150
155
  });
151
156
  });
152
157
 
@@ -191,7 +196,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
191
196
  );
192
197
  };
193
198
 
194
- const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpenChannel>({
199
+ const useCreateMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpenChannel>({
195
200
  channel,
196
201
  currentUserId,
197
202
  onResendFailedMessage,
@@ -208,7 +213,7 @@ const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpen
208
213
  | 'onDeleteMessage'
209
214
  | 'onResendFailedMessage'
210
215
  | 'onPressMediaMessage'
211
- >) => {
216
+ >): CreateMessagePressActions => {
212
217
  const { colors } = useUIKitTheme();
213
218
  const { STRINGS } = useLocalization();
214
219
  const toast = useToast();
@@ -217,161 +222,186 @@ const useGetMessagePressActions = <T extends SendbirdGroupChannel | SendbirdOpen
217
222
  const { clipboardService, fileService } = usePlatformService();
218
223
  const { sbOptions } = useSendbirdChat();
219
224
 
220
- const onFailureToReSend = (error: Error) => {
225
+ const onResendFailure = (error: Error) => {
221
226
  toast.show(STRINGS.TOAST.RESEND_MSG_ERROR, 'error');
222
227
  Logger.error(STRINGS.TOAST.RESEND_MSG_ERROR, error);
223
228
  };
224
229
 
225
- const handleFailedMessage = (message: HandleableMessage) => {
230
+ const onDeleteFailure = (error: Error) => {
231
+ toast.show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error');
232
+ Logger.error(STRINGS.TOAST.DELETE_MSG_ERROR, error);
233
+ };
234
+
235
+ const onCopyText = (message: HandleableMessage) => {
236
+ if (message.isUserMessage()) {
237
+ clipboardService.setString(message.message || '');
238
+ toast.show(STRINGS.TOAST.COPY_OK, 'success');
239
+ }
240
+ };
241
+
242
+ const onDownloadFile = (message: HandleableMessage) => {
243
+ if (message.isFileMessage()) {
244
+ if (toMegabyte(message.size) > 4) {
245
+ toast.show(STRINGS.TOAST.DOWNLOAD_START, 'success');
246
+ }
247
+
248
+ fileService
249
+ .save({ fileUrl: message.url, fileName: message.name, fileType: message.type })
250
+ .then((response) => {
251
+ toast.show(STRINGS.TOAST.DOWNLOAD_OK, 'success');
252
+ Logger.log('File saved to', response);
253
+ })
254
+ .catch((err) => {
255
+ toast.show(STRINGS.TOAST.DOWNLOAD_ERROR, 'error');
256
+ Logger.log('File save failure', err);
257
+ });
258
+ }
259
+ };
260
+
261
+ const onOpenFile = (message: HandleableMessage) => {
262
+ if (message.isFileMessage()) {
263
+ const fileType = getFileType(message.type || getFileExtension(message.name));
264
+ if (['image', 'video', 'audio'].includes(fileType)) {
265
+ onPressMediaMessage?.(message, () => onDeleteMessage(message), getAvailableUriFromFileMessage(message));
266
+ } else {
267
+ SBUUtils.openURL(message.url);
268
+ }
269
+ }
270
+ };
271
+
272
+ const openSheetForFailedMessage = (message: HandleableMessage) => {
226
273
  openSheet({
227
274
  sheetItems: [
228
275
  {
229
276
  title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_RETRY,
230
- onPress: () => {
231
- onResendFailedMessage(message).catch(onFailureToReSend);
232
- },
277
+ onPress: () => onResendFailedMessage(message).catch(onResendFailure),
233
278
  },
234
279
  {
235
280
  title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_REMOVE,
236
281
  titleColor: colors.ui.dialog.default.none.destructive,
237
- onPress: () => confirmDelete(message),
282
+ onPress: () => alertForMessageDelete(message),
238
283
  },
239
284
  ],
240
285
  });
241
286
  };
242
- const confirmDelete = (message: HandleableMessage) => {
287
+
288
+ const alertForMessageDelete = (message: HandleableMessage) => {
243
289
  alert({
244
290
  title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE,
245
291
  buttons: [
246
- {
247
- text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL,
248
- },
292
+ { text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL },
249
293
  {
250
294
  text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK,
251
295
  style: 'destructive',
252
296
  onPress: () => {
253
- onDeleteMessage(message).catch(() => toast.show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error'));
297
+ onDeleteMessage(message).catch(onDeleteFailure);
254
298
  },
255
299
  },
256
300
  ],
257
301
  });
258
302
  };
259
303
 
260
- return (msg: SendbirdMessage) => {
261
- if (!msg.isUserMessage() && !msg.isFileMessage()) {
262
- return { onPress: undefined, onLongPress: undefined };
263
- }
304
+ return ({ message }) => {
305
+ if (!message.isUserMessage() && !message.isFileMessage()) return {};
264
306
 
265
307
  const sheetItems: BottomSheetItem['sheetItems'] = [];
266
- const response: PressActions = {
267
- onPress: undefined,
268
- onLongPress: undefined,
269
- };
270
-
271
- if (msg.isUserMessage()) {
272
- sheetItems.push({
273
- icon: 'copy',
308
+ const menu = {
309
+ copy: (message: HandleableMessage) => ({
310
+ icon: 'copy' as const,
274
311
  title: STRINGS.LABELS.CHANNEL_MESSAGE_COPY,
275
- onPress: () => {
276
- clipboardService.setString(msg.message || '');
277
- toast.show(STRINGS.TOAST.COPY_OK, 'success');
278
- },
279
- });
280
- }
281
- if (!isVoiceMessage(msg) && msg.isFileMessage()) {
282
- sheetItems.push({
283
- icon: 'download',
312
+ onPress: () => onCopyText(message),
313
+ }),
314
+ edit: (message: HandleableMessage) => ({
315
+ icon: 'edit' as const,
316
+ title: STRINGS.LABELS.CHANNEL_MESSAGE_EDIT,
317
+ onPress: () => onEditMessage(message),
318
+ }),
319
+ delete: (message: HandleableMessage) => ({
320
+ disabled: message.threadInfo ? message.threadInfo.replyCount > 0 : undefined,
321
+ icon: 'delete' as const,
322
+ title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE,
323
+ onPress: () => alertForMessageDelete(message),
324
+ }),
325
+ reply: (message: HandleableMessage) => ({
326
+ disabled: Boolean(message.parentMessageId),
327
+ icon: 'reply' as const,
328
+ title: STRINGS.LABELS.CHANNEL_MESSAGE_REPLY,
329
+ onPress: () => onReplyMessage?.(message),
330
+ }),
331
+ download: (message: HandleableMessage) => ({
332
+ icon: 'download' as const,
284
333
  title: STRINGS.LABELS.CHANNEL_MESSAGE_SAVE,
285
- onPress: async () => {
286
- if (toMegabyte(msg.size) > 4) {
287
- toast.show(STRINGS.TOAST.DOWNLOAD_START, 'success');
288
- }
289
-
290
- fileService
291
- .save({ fileUrl: msg.url, fileName: msg.name, fileType: msg.type })
292
- .then((response) => {
293
- toast.show(STRINGS.TOAST.DOWNLOAD_OK, 'success');
294
- Logger.log('File saved to', response);
295
- })
296
- .catch((err) => {
297
- toast.show(STRINGS.TOAST.DOWNLOAD_ERROR, 'error');
298
- Logger.log('File save failure', err);
299
- });
300
- },
301
- });
302
- }
334
+ onPress: () => onDownloadFile(message),
335
+ }),
336
+ };
303
337
 
304
- if (!channel.isEphemeral) {
305
- if (isMyMessage(msg, currentUserId) && msg.sendingStatus === 'succeeded') {
306
- if (msg.isUserMessage()) {
307
- sheetItems.push({
308
- icon: 'edit',
309
- title: STRINGS.LABELS.CHANNEL_MESSAGE_EDIT,
310
- onPress: () => onEditMessage(msg),
311
- });
338
+ if (message.isUserMessage()) {
339
+ sheetItems.push(menu.copy(message));
340
+ if (!channel.isEphemeral) {
341
+ if (isMyMessage(message, currentUserId) && message.sendingStatus === 'succeeded') {
342
+ sheetItems.push(menu.edit(message));
343
+ sheetItems.push(menu.delete(message));
344
+ }
345
+ if (channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') {
346
+ sheetItems.push(menu.reply(message));
312
347
  }
313
- sheetItems.push({
314
- disabled: msg.threadInfo ? msg.threadInfo.replyCount > 0 : undefined,
315
- icon: 'delete',
316
- title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE,
317
- onPress: () => confirmDelete(msg),
318
- });
319
- }
320
- if (channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') {
321
- sheetItems.push({
322
- disabled: Boolean(msg.parentMessageId),
323
- icon: 'reply',
324
- title: STRINGS.LABELS.CHANNEL_MESSAGE_REPLY,
325
- onPress: () => onReplyMessage?.(msg),
326
- });
327
348
  }
328
349
  }
329
350
 
330
- if (msg.isFileMessage()) {
331
- const fileType = getFileType(msg.type || getFileExtension(msg.name));
332
- switch (fileType) {
333
- case 'image':
334
- case 'video':
335
- case 'audio': {
336
- response.onPress = () => {
337
- onPressMediaMessage?.(msg, () => onDeleteMessage(msg), getAvailableUriFromFileMessage(msg));
338
- };
339
- break;
351
+ if (message.isFileMessage()) {
352
+ if (!isVoiceMessage(message)) {
353
+ sheetItems.push(menu.download(message));
354
+ }
355
+ if (!channel.isEphemeral) {
356
+ if (isMyMessage(message, currentUserId) && message.sendingStatus === 'succeeded') {
357
+ sheetItems.push(menu.delete(message));
340
358
  }
341
- default: {
342
- response.onPress = () => SBUUtils.openURL(msg.url);
343
- break;
359
+ if (channel.isGroupChannel() && sbOptions.uikit.groupChannel.channel.replyType === 'quote_reply') {
360
+ sheetItems.push(menu.reply(message));
344
361
  }
345
362
  }
346
363
  }
347
364
 
348
- if (sheetItems.length > 0) {
349
- response.onLongPress = () => {
350
- openSheet({
351
- sheetItems,
352
- HeaderComponent: shouldRenderReaction(
353
- channel,
354
- sbOptions.uikitWithAppInfo.groupChannel.channel.enableReactions,
355
- )
356
- ? ({ onClose }) => <ReactionAddons.BottomSheet message={msg} channel={channel} onClose={onClose} />
357
- : undefined,
358
- });
359
- };
360
- }
365
+ const bottomSheetItem: BottomSheetItem = {
366
+ sheetItems,
367
+ HeaderComponent: shouldRenderReaction(channel, sbOptions.uikitWithAppInfo.groupChannel.channel.enableReactions)
368
+ ? ({ onClose }) => <ReactionAddons.BottomSheet message={message} channel={channel} onClose={onClose} />
369
+ : undefined,
370
+ };
361
371
 
362
- if (msg.sendingStatus === 'failed') {
363
- response.onLongPress = () => handleFailedMessage(msg);
364
- response.onPress = () => {
365
- onResendFailedMessage(msg).catch(onFailureToReSend);
366
- };
367
- }
372
+ switch (true) {
373
+ case message.sendingStatus === 'pending': {
374
+ return {
375
+ onPress: undefined,
376
+ onLongPress: undefined,
377
+ bottomSheetItem: undefined,
378
+ };
379
+ }
368
380
 
369
- if (msg.sendingStatus === 'pending') {
370
- response.onLongPress = undefined;
371
- response.onPress = undefined;
372
- }
381
+ case message.sendingStatus === 'failed': {
382
+ return {
383
+ onPress: () => onResendFailedMessage(message).catch(onResendFailure),
384
+ onLongPress: () => openSheetForFailedMessage(message),
385
+ bottomSheetItem,
386
+ };
387
+ }
388
+
389
+ case message.isFileMessage(): {
390
+ return {
391
+ onPress: () => onOpenFile(message),
392
+ onLongPress: () => openSheet(bottomSheetItem),
393
+ bottomSheetItem,
394
+ };
395
+ }
373
396
 
374
- return response;
397
+ default: {
398
+ return {
399
+ onPress: undefined,
400
+ onLongPress: () => openSheet(bottomSheetItem),
401
+ bottomSheetItem,
402
+ };
403
+ }
404
+ }
375
405
  };
376
406
  };
377
407
 
@@ -1,7 +1,13 @@
1
- import React, { useRef } from 'react';
1
+ import React, { useContext, useEffect, useRef } from 'react';
2
2
 
3
3
  import type { GroupChannelMessageProps, RegexTextPattern } from '@sendbird/uikit-react-native-foundation';
4
- import { Box, GroupChannelMessage, Text, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
4
+ import {
5
+ Box,
6
+ GroupChannelMessage,
7
+ Text,
8
+ TypingIndicatorBubble,
9
+ useUIKitTheme,
10
+ } from '@sendbird/uikit-react-native-foundation';
5
11
  import {
6
12
  SendbirdAdminMessage,
7
13
  SendbirdFileMessage,
@@ -17,9 +23,11 @@ import {
17
23
  } from '@sendbird/uikit-utils';
18
24
 
19
25
  import { VOICE_MESSAGE_META_ARRAY_DURATION_KEY } from '../../constants';
26
+ import { GroupChannelContexts } from '../../domain/groupChannel/module/moduleContext';
20
27
  import type { GroupChannelProps } from '../../domain/groupChannel/types';
21
28
  import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext';
22
29
  import SBUUtils from '../../libs/SBUUtils';
30
+ import { TypingIndicatorType } from '../../types';
23
31
  import { ReactionAddons } from '../ReactionAddons';
24
32
  import GroupChannelMessageDateSeparator from './GroupChannelMessageDateSeparator';
25
33
  import GroupChannelMessageFocusAnimation from './GroupChannelMessageFocusAnimation';
@@ -292,4 +300,28 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
292
300
  );
293
301
  };
294
302
 
303
+ export const GroupChannelTypingIndicatorBubble = () => {
304
+ const { sbOptions } = useSendbirdChat();
305
+ const { publish } = useContext(GroupChannelContexts.PubSub);
306
+ const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator);
307
+
308
+ const shouldRenderBubble = useIIFE(() => {
309
+ if (typingUsers.length === 0) return false;
310
+ if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return false;
311
+ if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has(TypingIndicatorType.Bubble)) return false;
312
+ return true;
313
+ });
314
+
315
+ useEffect(() => {
316
+ if (shouldRenderBubble) publish({ type: 'TYPING_BUBBLE_RENDERED' });
317
+ }, [shouldRenderBubble]);
318
+
319
+ if (!shouldRenderBubble) return null;
320
+ return (
321
+ <Box paddingHorizontal={16} marginTop={4} marginBottom={16}>
322
+ <TypingIndicatorBubble typingUsers={typingUsers} />
323
+ </Box>
324
+ );
325
+ };
326
+
295
327
  export default React.memo(GroupChannelMessageRenderer);
@@ -4,7 +4,8 @@ import { View } from 'react-native';
4
4
  import { Header, Icon, createStyleSheet, useHeaderStyle } from '@sendbird/uikit-react-native-foundation';
5
5
 
6
6
  import ChannelCover from '../../../components/ChannelCover';
7
- import { useLocalization } from '../../../hooks/useContext';
7
+ import { useLocalization, useSendbirdChat } from '../../../hooks/useContext';
8
+ import { TypingIndicatorType } from '../../../types';
8
9
  import { GroupChannelContexts } from '../module/moduleContext';
9
10
  import type { GroupChannelProps } from '../types';
10
11
 
@@ -13,11 +14,21 @@ const GroupChannelHeader = ({
13
14
  onPressHeaderLeft,
14
15
  onPressHeaderRight,
15
16
  }: GroupChannelProps['Header']) => {
17
+ const { sbOptions } = useSendbirdChat();
16
18
  const { headerTitle, channel } = useContext(GroupChannelContexts.Fragment);
17
19
  const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator);
18
20
  const { STRINGS } = useLocalization();
19
21
  const { HeaderComponent } = useHeaderStyle();
20
- const subtitle = STRINGS.LABELS.TYPING_INDICATOR_TYPINGS(typingUsers);
22
+
23
+ const renderSubtitle = () => {
24
+ const subtitle = STRINGS.LABELS.TYPING_INDICATOR_TYPINGS(typingUsers);
25
+
26
+ if (!subtitle) return null;
27
+ if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null;
28
+ if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has(TypingIndicatorType.Text)) return null;
29
+
30
+ return <Header.Subtitle style={styles.subtitle}>{subtitle}</Header.Subtitle>;
31
+ };
21
32
 
22
33
  const isHidden = shouldHideRight();
23
34
 
@@ -29,7 +40,7 @@ const GroupChannelHeader = ({
29
40
  <ChannelCover channel={channel} size={34} containerStyle={styles.avatarGroup} />
30
41
  <View style={{ flexShrink: 1 }}>
31
42
  <Header.Title h2>{headerTitle}</Header.Title>
32
- {Boolean(subtitle) && subtitle && <Header.Subtitle style={styles.subtitle}>{subtitle}</Header.Subtitle>}
43
+ {renderSubtitle()}
33
44
  </View>
34
45
  </View>
35
46
  }
@@ -73,6 +73,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
73
73
  useEffect(() => {
74
74
  return subscribe(({ type }) => {
75
75
  switch (type) {
76
+ case 'TYPING_BUBBLE_RENDERED':
76
77
  case 'MESSAGES_RECEIVED': {
77
78
  if (!props.scrolledAwayFromBottom) {
78
79
  scrollToBottom(true);
@@ -183,4 +183,8 @@ export type GroupChannelPubSubContextPayload =
183
183
  data: {
184
184
  messages: SendbirdMessage[];
185
185
  };
186
+ }
187
+ | {
188
+ type: 'TYPING_BUBBLE_RENDERED';
189
+ data?: undefined;
186
190
  };
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
 
3
3
  import { ReplyType } from '@sendbird/chat/message';
4
4
  import { useGroupChannelMessages } from '@sendbird/uikit-chat-hooks';
5
+ import { Box } from '@sendbird/uikit-react-native-foundation';
5
6
  import {
6
7
  NOOP,
7
8
  PASS,
@@ -14,7 +15,9 @@ import {
14
15
  useRefTracker,
15
16
  } from '@sendbird/uikit-utils';
16
17
 
17
- import GroupChannelMessageRenderer from '../components/GroupChannelMessageRenderer';
18
+ import GroupChannelMessageRenderer, {
19
+ GroupChannelTypingIndicatorBubble,
20
+ } from '../components/GroupChannelMessageRenderer';
18
21
  import NewMessagesButton from '../components/NewMessagesButton';
19
22
  import ScrollToBottomButton from '../components/ScrollToBottomButton';
20
23
  import StatusComposition from '../components/StatusComposition';
@@ -123,8 +126,13 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
123
126
  }, []);
124
127
 
125
128
  const renderItem: GroupChannelProps['MessageList']['renderMessage'] = useFreshCallback((props) => {
126
- if (renderMessage) return renderMessage(props);
127
- return <GroupChannelMessageRenderer {...props} />;
129
+ const content = renderMessage ? renderMessage(props) : <GroupChannelMessageRenderer {...props} />;
130
+ return (
131
+ <Box>
132
+ {content}
133
+ {props.isFirstItem && !hasNext() && <GroupChannelTypingIndicatorBubble />}
134
+ </Box>
135
+ );
128
136
  });
129
137
 
130
138
  const memoizedFlatListProps = useMemo(
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ import { Platform } from 'react-native';
2
2
 
3
3
  import { Logger } from '@sendbird/uikit-utils';
4
4
 
5
+ import { PromisePolyfill } from './utils/promise';
6
+
5
7
  /** Components **/
6
8
  export { default as ChannelInput } from './components/ChannelInput';
7
9
  export { default as ChannelMessageList } from './components/ChannelMessageList';
@@ -132,8 +134,10 @@ export { default as SendbirdUIKitContainer, SendbirdUIKit } from './containers/S
132
134
  export type { SendbirdUIKitContainerProps } from './containers/SendbirdUIKitContainer';
133
135
  export { default as SBUError } from './libs/SBUError';
134
136
  export { default as SBUUtils } from './libs/SBUUtils';
135
-
136
137
  export * from './types';
137
138
 
138
139
  Logger.setLogLevel(__DEV__ ? 'warn' : 'none');
139
140
  Logger.setTitle(`[UIKIT_${Platform.OS}]`);
141
+
142
+ // NOTE: In Hermes, not all implementations of Promise are included
143
+ PromisePolyfill.apply();
package/src/types.ts CHANGED
@@ -28,3 +28,8 @@ export type Range = {
28
28
  start: number;
29
29
  end: number;
30
30
  };
31
+
32
+ export enum TypingIndicatorType {
33
+ Text = 'text',
34
+ Bubble = 'bubble',
35
+ }