@sendbird/uikit-react-native 3.2.0 → 3.4.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 (116) 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/FileViewer/FileViewerContent.js +140 -0
  10. package/lib/commonjs/components/FileViewer/FileViewerContent.js.map +1 -0
  11. package/lib/commonjs/components/FileViewer/FileViewerFooter.js +82 -0
  12. package/lib/commonjs/components/FileViewer/FileViewerFooter.js.map +1 -0
  13. package/lib/commonjs/components/FileViewer/FileViewerHeader.js +93 -0
  14. package/lib/commonjs/components/FileViewer/FileViewerHeader.js.map +1 -0
  15. package/lib/commonjs/components/FileViewer/index.js +133 -0
  16. package/lib/commonjs/components/FileViewer/index.js.map +1 -0
  17. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js +34 -1
  18. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js.map +1 -1
  19. package/lib/commonjs/components/ReactionAddons/BottomSheetReactionAddon.js.map +1 -1
  20. package/lib/commonjs/domain/groupChannel/component/GroupChannelHeader.js +14 -4
  21. package/lib/commonjs/domain/groupChannel/component/GroupChannelHeader.js.map +1 -1
  22. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js +11 -9
  23. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  24. package/lib/commonjs/domain/groupChannel/types.js.map +1 -1
  25. package/lib/commonjs/domain/messageSearch/component/MessageSearchHeader.js +4 -1
  26. package/lib/commonjs/domain/messageSearch/component/MessageSearchHeader.js.map +1 -1
  27. package/lib/commonjs/domain/openChannelCreate/component/OpenChannelCreateProfileInput.js +4 -2
  28. package/lib/commonjs/domain/openChannelCreate/component/OpenChannelCreateProfileInput.js.map +1 -1
  29. package/lib/commonjs/fragments/createGroupChannelFragment.js +18 -16
  30. package/lib/commonjs/fragments/createGroupChannelFragment.js.map +1 -1
  31. package/lib/commonjs/index.js +4 -0
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/commonjs/types.js +7 -0
  34. package/lib/commonjs/types.js.map +1 -1
  35. package/lib/commonjs/utils/promise.js +138 -0
  36. package/lib/commonjs/utils/promise.js.map +1 -0
  37. package/lib/commonjs/version.js +1 -1
  38. package/lib/commonjs/version.js.map +1 -1
  39. package/lib/module/components/ChannelInput/EditInput.js +3 -12
  40. package/lib/module/components/ChannelInput/EditInput.js.map +1 -1
  41. package/lib/module/components/ChannelInput/SendInput.js +3 -12
  42. package/lib/module/components/ChannelInput/SendInput.js.map +1 -1
  43. package/lib/module/components/ChannelInput/index.js +32 -5
  44. package/lib/module/components/ChannelInput/index.js.map +1 -1
  45. package/lib/module/components/ChannelMessageList/index.js +148 -116
  46. package/lib/module/components/ChannelMessageList/index.js.map +1 -1
  47. package/lib/module/components/FileViewer/FileViewerContent.js +130 -0
  48. package/lib/module/components/FileViewer/FileViewerContent.js.map +1 -0
  49. package/lib/module/components/FileViewer/FileViewerFooter.js +74 -0
  50. package/lib/module/components/FileViewer/FileViewerFooter.js.map +1 -0
  51. package/lib/module/components/FileViewer/FileViewerHeader.js +85 -0
  52. package/lib/module/components/FileViewer/FileViewerHeader.js.map +1 -0
  53. package/lib/module/components/FileViewer/index.js +123 -0
  54. package/lib/module/components/FileViewer/index.js.map +1 -0
  55. package/lib/module/components/GroupChannelMessageRenderer/index.js +34 -2
  56. package/lib/module/components/GroupChannelMessageRenderer/index.js.map +1 -1
  57. package/lib/module/components/ReactionAddons/BottomSheetReactionAddon.js.map +1 -1
  58. package/lib/module/domain/groupChannel/component/GroupChannelHeader.js +15 -5
  59. package/lib/module/domain/groupChannel/component/GroupChannelHeader.js.map +1 -1
  60. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js +11 -9
  61. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  62. package/lib/module/domain/groupChannel/types.js.map +1 -1
  63. package/lib/module/domain/messageSearch/component/MessageSearchHeader.js +4 -1
  64. package/lib/module/domain/messageSearch/component/MessageSearchHeader.js.map +1 -1
  65. package/lib/module/domain/openChannelCreate/component/OpenChannelCreateProfileInput.js +4 -2
  66. package/lib/module/domain/openChannelCreate/component/OpenChannelCreateProfileInput.js.map +1 -1
  67. package/lib/module/fragments/createGroupChannelFragment.js +19 -17
  68. package/lib/module/fragments/createGroupChannelFragment.js.map +1 -1
  69. package/lib/module/index.js +4 -0
  70. package/lib/module/index.js.map +1 -1
  71. package/lib/module/types.js +5 -1
  72. package/lib/module/types.js.map +1 -1
  73. package/lib/module/utils/promise.js +132 -0
  74. package/lib/module/utils/promise.js.map +1 -0
  75. package/lib/module/version.js +1 -1
  76. package/lib/module/version.js.map +1 -1
  77. package/lib/typescript/src/components/ChannelInput/index.d.ts +2 -0
  78. package/lib/typescript/src/components/ChannelMessageList/index.d.ts +4 -1
  79. package/lib/typescript/src/components/FileViewer/FileViewerContent.d.ts +13 -0
  80. package/lib/typescript/src/components/FileViewer/FileViewerFooter.d.ts +9 -0
  81. package/lib/typescript/src/components/FileViewer/FileViewerHeader.d.ts +10 -0
  82. package/lib/typescript/src/components/{FileViewer.d.ts → FileViewer/index.d.ts} +5 -1
  83. package/lib/typescript/src/components/GroupChannelMessageRenderer/index.d.ts +3 -0
  84. package/lib/typescript/src/components/OpenChannelMessageRenderer/index.d.ts +2 -0
  85. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  86. package/lib/typescript/src/domain/groupChannel/component/GroupChannelMessageList.d.ts +2 -2
  87. package/lib/typescript/src/domain/groupChannel/types.d.ts +5 -2
  88. package/lib/typescript/src/types.d.ts +4 -0
  89. package/lib/typescript/src/utils/promise.d.ts +7 -0
  90. package/lib/typescript/src/version.d.ts +1 -1
  91. package/package.json +8 -7
  92. package/src/components/ChannelInput/EditInput.tsx +3 -15
  93. package/src/components/ChannelInput/SendInput.tsx +2 -9
  94. package/src/components/ChannelInput/index.tsx +27 -4
  95. package/src/components/ChannelMessageList/index.tsx +145 -115
  96. package/src/components/FileViewer/FileViewerContent.tsx +141 -0
  97. package/src/components/FileViewer/FileViewerFooter.tsx +73 -0
  98. package/src/components/FileViewer/FileViewerHeader.tsx +86 -0
  99. package/src/components/FileViewer/index.tsx +139 -0
  100. package/src/components/GroupChannelMessageRenderer/index.tsx +34 -2
  101. package/src/components/ReactionAddons/BottomSheetReactionAddon.tsx +3 -2
  102. package/src/domain/groupChannel/component/GroupChannelHeader.tsx +14 -3
  103. package/src/domain/groupChannel/component/GroupChannelMessageList.tsx +8 -6
  104. package/src/domain/groupChannel/types.ts +6 -2
  105. package/src/domain/messageSearch/component/MessageSearchHeader.tsx +4 -1
  106. package/src/domain/openChannelCreate/component/OpenChannelCreateProfileInput.tsx +4 -2
  107. package/src/fragments/createGroupChannelFragment.tsx +25 -15
  108. package/src/index.ts +5 -1
  109. package/src/types.ts +5 -0
  110. package/src/utils/promise.ts +139 -0
  111. package/src/version.ts +1 -1
  112. package/lib/commonjs/components/FileViewer.js +0 -300
  113. package/lib/commonjs/components/FileViewer.js.map +0 -1
  114. package/lib/module/components/FileViewer.js +0 -291
  115. package/lib/module/components/FileViewer.js.map +0 -1
  116. package/src/components/FileViewer.tsx +0 -288
@@ -1,11 +1,5 @@
1
1
  import React, { forwardRef } from 'react';
2
- import {
3
- NativeSyntheticEvent,
4
- Platform,
5
- TextInput as RNTextInput,
6
- TextInputSelectionChangeEventData,
7
- View,
8
- } from 'react-native';
2
+ import { NativeSyntheticEvent, TextInput as RNTextInput, TextInputSelectionChangeEventData, View } from 'react-native';
9
3
 
10
4
  import { MentionType } from '@sendbird/chat/message';
11
5
  import { Button, TextInput, createStyleSheet, useToast } from '@sendbird/uikit-react-native-foundation';
@@ -27,6 +21,7 @@ interface EditInputProps extends ChannelInputProps {
27
21
 
28
22
  const EditInput = forwardRef<RNTextInput, EditInputProps>(function EditInput(
29
23
  {
24
+ style,
30
25
  text,
31
26
  onChangeText,
32
27
  messageToEdit,
@@ -81,7 +76,7 @@ const EditInput = forwardRef<RNTextInput, EditInputProps>(function EditInput(
81
76
  editable={!inputDisabled}
82
77
  autoFocus={autoFocus}
83
78
  onChangeText={onChangeText}
84
- style={styles.input}
79
+ style={style}
85
80
  placeholder={STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE}
86
81
  onSelectionChange={onSelectionChange}
87
82
  >
@@ -112,13 +107,6 @@ const styles = createStyleSheet({
112
107
  flexDirection: 'column',
113
108
  alignItems: 'center',
114
109
  },
115
- input: {
116
- flex: 1,
117
- marginRight: 4,
118
- minHeight: 36,
119
- maxHeight: 36 * Platform.select({ ios: 2.5, default: 2 }),
120
- borderRadius: 20,
121
- },
122
110
  inputWrapper: {
123
111
  flexDirection: 'row',
124
112
  },
@@ -1,7 +1,6 @@
1
1
  import React, { forwardRef } from 'react';
2
2
  import {
3
3
  NativeSyntheticEvent,
4
- Platform,
5
4
  TextInput as RNTextInput,
6
5
  TextInputSelectionChangeEventData,
7
6
  TouchableOpacity,
@@ -38,6 +37,7 @@ interface SendInputProps extends ChannelInputProps {
38
37
 
39
38
  const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
40
39
  {
40
+ style,
41
41
  VoiceMessageInput,
42
42
  MessageToReplyPreview,
43
43
  AttachmentsButton,
@@ -173,7 +173,7 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
173
173
  onSelectionChange={onSelectionChange}
174
174
  editable={!inputDisabled}
175
175
  onChangeText={onChangeText}
176
- style={styles.input}
176
+ style={style}
177
177
  placeholder={getPlaceholder()}
178
178
  >
179
179
  {mentionManager.textToMentionedComponents(
@@ -284,13 +284,6 @@ const styles = createStyleSheet({
284
284
  alignItems: 'center',
285
285
  flexDirection: 'row',
286
286
  },
287
- input: {
288
- flex: 1,
289
- marginRight: 4,
290
- minHeight: 36,
291
- maxHeight: 36 * Platform.select({ ios: 2.5, default: 2 }),
292
- borderRadius: 20,
293
- },
294
287
  sendIcon: {
295
288
  marginLeft: 4,
296
289
  padding: 4,
@@ -1,5 +1,5 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { KeyboardAvoidingView, Platform, TextInput, View } from 'react-native';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { KeyboardAvoidingView, Platform, StyleProp, StyleSheet, TextInput, TextStyle, View } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
4
 
5
5
  import { createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
@@ -39,6 +39,9 @@ export type SuggestedMentionListProps = {
39
39
  };
40
40
 
41
41
  export type ChannelInputProps = {
42
+ // style
43
+ style?: StyleProp<TextStyle>;
44
+
42
45
  // default
43
46
  channel: SendbirdBaseChannel;
44
47
  shouldRenderInput: boolean;
@@ -85,7 +88,7 @@ const ChannelInput = (props: ChannelInputProps) => {
85
88
  const { channel, keyboardAvoidOffset, messageToEdit, setMessageToEdit } = props;
86
89
 
87
90
  const { top, left, right, bottom } = useSafeAreaInsets();
88
- const { colors } = useUIKitTheme();
91
+ const { colors, typography } = useUIKitTheme();
89
92
  const { sbOptions, mentionManager } = useSendbirdChat();
90
93
 
91
94
  const { selection, onSelectionChange, textInputRef, text, onChangeText, mentionedUsers } = useMentionTextInput({
@@ -98,11 +101,18 @@ const ChannelInput = (props: ChannelInputProps) => {
98
101
 
99
102
  const mentionAvailable =
100
103
  sbOptions.uikit.groupChannel.channel.enableMention && channel.isGroupChannel() && !channel.isBroadcast;
101
-
102
104
  const inputKeyToRemount = GET_INPUT_KEY(mentionAvailable ? mentionedUsers.length === 0 : false);
103
105
 
104
106
  const [inputHeight, setInputHeight] = useState(styles.inputDefault.height);
105
107
 
108
+ const fontStyle = useMemo(() => {
109
+ if (!typography.body3.fontSize) return typography.body3;
110
+ // NOTE: iOS does not support textAlignVertical, so we should adjust lineHeight to center the text in multiline TextInput.
111
+ return { ...typography.body3, lineHeight: typography.body3.fontSize * 1.275, textAlignVertical: 'center' };
112
+ }, [typography.body3.fontSize]);
113
+
114
+ const textInputStyle = StyleSheet.flatten([styles.input, fontStyle, props.style]);
115
+
106
116
  useTypingTrigger(text, channel);
107
117
  useTextClearOnDisabled(onChangeText, props.inputDisabled);
108
118
  useAutoFocusOnEditMode(textInputRef, messageToEdit);
@@ -138,6 +148,7 @@ const ChannelInput = (props: ChannelInputProps) => {
138
148
  VoiceMessageInput={props.VoiceMessageInput ?? VoiceMessageInput}
139
149
  AttachmentsButton={props.AttachmentsButton ?? AttachmentsButton}
140
150
  MessageToReplyPreview={props.MessageToReplyPreview ?? MessageToReplyPreview}
151
+ style={textInputStyle}
141
152
  />
142
153
  )}
143
154
  {inputMode === 'edit' && messageToEdit && (
@@ -152,6 +163,7 @@ const ChannelInput = (props: ChannelInputProps) => {
152
163
  mentionedUsers={mentionedUsers}
153
164
  messageToEdit={messageToEdit}
154
165
  setMessageToEdit={setMessageToEdit}
166
+ style={textInputStyle}
155
167
  />
156
168
  )}
157
169
  </View>
@@ -211,6 +223,17 @@ const styles = createStyleSheet({
211
223
  inputDefault: {
212
224
  height: 56,
213
225
  },
226
+ input: {
227
+ flex: 1,
228
+ marginRight: 4,
229
+ borderRadius: 20,
230
+ paddingTop: 8,
231
+ paddingBottom: 8,
232
+ minHeight: 36,
233
+ // Android - padding area is hidden
234
+ // iOS - padding area is visible
235
+ maxHeight: Platform.select({ ios: 36 * 2 + 16, android: 36 * 2 }),
236
+ },
214
237
  });
215
238
 
216
239
  export default React.memo(ChannelInput);
@@ -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;
@@ -58,7 +59,7 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
58
59
  onEditMessage: (message: HandleableMessage) => void;
59
60
  onReplyMessage?: (message: HandleableMessage) => void; // only available on group channel
60
61
  onDeleteMessage: (message: HandleableMessage) => Promise<void>;
61
- onResendFailedMessage: (failedMessage: HandleableMessage) => Promise<void>;
62
+ onResendFailedMessage: (failedMessage: HandleableMessage) => Promise<HandleableMessage | void>;
62
63
  onPressParentMessage?: (parentMessage: SendbirdMessage) => void;
63
64
  onPressMediaMessage?: (message: SendbirdFileMessage, deleteMessage: () => Promise<void>, uri: string) => void;
64
65
 
@@ -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
 
@@ -0,0 +1,141 @@
1
+ import { ReactNativeZoomableView, ReactNativeZoomableViewProps } from '@openspacelabs/react-native-zoomable-view';
2
+ import React, { useLayoutEffect, useRef, useState } from 'react';
3
+ import { ImageProps, ImageStyle, ImageURISource, StyleProp, StyleSheet, useWindowDimensions } from 'react-native';
4
+
5
+ import {
6
+ Box,
7
+ Image,
8
+ LoadingSpinner,
9
+ createStyleSheet,
10
+ useHeaderStyle,
11
+ useUIKitTheme,
12
+ } from '@sendbird/uikit-react-native-foundation';
13
+ import { FileType, useIIFE } from '@sendbird/uikit-utils';
14
+
15
+ import { usePlatformService } from '../../hooks/useContext';
16
+ import SBUUtils from '../../libs/SBUUtils';
17
+
18
+ type Props = {
19
+ type: FileType;
20
+ src: string;
21
+ topInset?: number;
22
+ bottomInset?: number;
23
+ maxZoom?: number;
24
+ minZoom?: number;
25
+ onPress?: () => void;
26
+ };
27
+ const FileViewerContent = ({ type, src, topInset = 0, bottomInset = 0, maxZoom = 4, minZoom = 1, onPress }: Props) => {
28
+ const [loading, setLoading] = useState(true);
29
+
30
+ const { defaultHeight } = useHeaderStyle();
31
+ const { mediaService } = usePlatformService();
32
+ const { palette } = useUIKitTheme();
33
+
34
+ const source = { uri: src };
35
+ const onLoadEnd = () => setLoading(false);
36
+ const mediaViewer = useIIFE(() => {
37
+ switch (type) {
38
+ case 'image': {
39
+ return (
40
+ <ZoomableImageView
41
+ source={source}
42
+ style={StyleSheet.absoluteFill}
43
+ resizeMode={'contain'}
44
+ onLoadEnd={onLoadEnd}
45
+ zoomProps={{
46
+ minZoom,
47
+ maxZoom,
48
+ onTouchEnd: onPress,
49
+ }}
50
+ />
51
+ );
52
+ }
53
+
54
+ case 'video':
55
+ case 'audio': {
56
+ return (
57
+ <mediaService.VideoComponent
58
+ source={source}
59
+ style={[StyleSheet.absoluteFill, { top: topInset, bottom: defaultHeight + bottomInset }]}
60
+ resizeMode={'contain'}
61
+ onLoad={onLoadEnd}
62
+ />
63
+ );
64
+ }
65
+
66
+ default: {
67
+ return null;
68
+ }
69
+ }
70
+ });
71
+
72
+ return (
73
+ <Box style={styles.container}>
74
+ {mediaViewer}
75
+ {loading && <LoadingSpinner style={{ position: 'absolute' }} size={40} color={palette.primary300} />}
76
+ </Box>
77
+ );
78
+ };
79
+
80
+ const ZoomableImageView = ({
81
+ zoomProps,
82
+ ...props
83
+ }: {
84
+ source: ImageURISource;
85
+ style: StyleProp<ImageStyle>;
86
+ resizeMode: ImageProps['resizeMode'];
87
+ onLoadEnd: () => void;
88
+ zoomProps?: ReactNativeZoomableViewProps;
89
+ }) => {
90
+ const { width, height } = useWindowDimensions();
91
+
92
+ const imageSize = useRef<{ width: number; height: number }>();
93
+ const [contentSizeProps, setContentSizeProps] = useState<ReactNativeZoomableViewProps>({
94
+ contentWidth: width,
95
+ contentHeight: height,
96
+ });
97
+
98
+ useLayoutEffect(() => {
99
+ SBUUtils.safeRun(async () => {
100
+ if (props.source.uri) {
101
+ const image = imageSize.current ?? (await SBUUtils.getImageSize(props.source.uri));
102
+ imageSize.current = image;
103
+
104
+ const viewRatio = width / height;
105
+ const imageRatio = image.width / image.height;
106
+
107
+ const fitDirection = viewRatio > imageRatio ? 'height' : 'width';
108
+ const ratio = fitDirection === 'height' ? height / image.height : width / image.width;
109
+ const actualSize = { width: image.width * ratio, height: image.height * ratio };
110
+
111
+ setContentSizeProps({
112
+ contentWidth: actualSize.width,
113
+ contentHeight: actualSize.height,
114
+ });
115
+ }
116
+ });
117
+ }, [props.source.uri, width, height]);
118
+
119
+ return (
120
+ <ReactNativeZoomableView
121
+ visualTouchFeedbackEnabled={false}
122
+ style={{ width, height }}
123
+ initialZoom={1}
124
+ {...contentSizeProps}
125
+ {...zoomProps}
126
+ >
127
+ <Image {...props} />
128
+ </ReactNativeZoomableView>
129
+ );
130
+ };
131
+
132
+ const styles = createStyleSheet({
133
+ container: {
134
+ zIndex: -1,
135
+ flex: 1,
136
+ alignItems: 'center',
137
+ justifyContent: 'center',
138
+ },
139
+ });
140
+
141
+ export default FileViewerContent;