@sendbird/uikit-react-native 1.1.1 → 1.1.3

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 (92) hide show
  1. package/lib/commonjs/components/FileViewer.js +8 -3
  2. package/lib/commonjs/components/FileViewer.js.map +1 -1
  3. package/lib/commonjs/components/MessageRenderer/FileMessage/ImageFileMessage.js +47 -15
  4. package/lib/commonjs/components/MessageRenderer/FileMessage/ImageFileMessage.js.map +1 -1
  5. package/lib/commonjs/components/MessageRenderer/FileMessage/VideoFileMessage.js +51 -34
  6. package/lib/commonjs/components/MessageRenderer/FileMessage/VideoFileMessage.js.map +1 -1
  7. package/lib/commonjs/components/MessageRenderer/MessageIncomingSenderName.js +2 -1
  8. package/lib/commonjs/components/MessageRenderer/MessageIncomingSenderName.js.map +1 -1
  9. package/lib/commonjs/components/MessageRenderer/index.js +7 -2
  10. package/lib/commonjs/components/MessageRenderer/index.js.map +1 -1
  11. package/lib/commonjs/contexts/SendbirdChat.js +1 -1
  12. package/lib/commonjs/contexts/SendbirdChat.js.map +1 -1
  13. package/lib/commonjs/domain/groupChannel/component/GroupChannelInput/SendInput.js +39 -6
  14. package/lib/commonjs/domain/groupChannel/component/GroupChannelInput/SendInput.js.map +1 -1
  15. package/lib/commonjs/domain/groupChannelSettings/component/GroupChannelSettingsInfo.js +1 -1
  16. package/lib/commonjs/domain/groupChannelSettings/component/GroupChannelSettingsInfo.js.map +1 -1
  17. package/lib/commonjs/domain/groupChannelSettings/module/moduleContext.js +35 -2
  18. package/lib/commonjs/domain/groupChannelSettings/module/moduleContext.js.map +1 -1
  19. package/lib/commonjs/fragments/createGroupChannelListFragment.js +1 -1
  20. package/lib/commonjs/fragments/createGroupChannelListFragment.js.map +1 -1
  21. package/lib/commonjs/libs/SBUError.js +41 -0
  22. package/lib/commonjs/libs/SBUError.js.map +1 -0
  23. package/lib/commonjs/libs/SBUUtils.js +20 -0
  24. package/lib/commonjs/libs/SBUUtils.js.map +1 -0
  25. package/lib/commonjs/localization/StringSet.type.js +6 -0
  26. package/lib/commonjs/localization/StringSet.type.js.map +1 -1
  27. package/lib/commonjs/platform/createFileService.expo.js +6 -4
  28. package/lib/commonjs/platform/createFileService.expo.js.map +1 -1
  29. package/lib/commonjs/platform/createFileService.native.js +20 -8
  30. package/lib/commonjs/platform/createFileService.native.js.map +1 -1
  31. package/lib/commonjs/platform/types.js +4 -0
  32. package/lib/commonjs/platform/types.js.map +1 -1
  33. package/lib/commonjs/version.js +1 -1
  34. package/lib/commonjs/version.js.map +1 -1
  35. package/lib/module/components/FileViewer.js +9 -4
  36. package/lib/module/components/FileViewer.js.map +1 -1
  37. package/lib/module/components/MessageRenderer/FileMessage/ImageFileMessage.js +48 -17
  38. package/lib/module/components/MessageRenderer/FileMessage/ImageFileMessage.js.map +1 -1
  39. package/lib/module/components/MessageRenderer/FileMessage/VideoFileMessage.js +52 -35
  40. package/lib/module/components/MessageRenderer/FileMessage/VideoFileMessage.js.map +1 -1
  41. package/lib/module/components/MessageRenderer/MessageIncomingSenderName.js +2 -1
  42. package/lib/module/components/MessageRenderer/MessageIncomingSenderName.js.map +1 -1
  43. package/lib/module/components/MessageRenderer/index.js +7 -2
  44. package/lib/module/components/MessageRenderer/index.js.map +1 -1
  45. package/lib/module/contexts/SendbirdChat.js +1 -1
  46. package/lib/module/contexts/SendbirdChat.js.map +1 -1
  47. package/lib/module/domain/groupChannel/component/GroupChannelInput/SendInput.js +38 -7
  48. package/lib/module/domain/groupChannel/component/GroupChannelInput/SendInput.js.map +1 -1
  49. package/lib/module/domain/groupChannelSettings/component/GroupChannelSettingsInfo.js +1 -1
  50. package/lib/module/domain/groupChannelSettings/component/GroupChannelSettingsInfo.js.map +1 -1
  51. package/lib/module/domain/groupChannelSettings/module/moduleContext.js +34 -3
  52. package/lib/module/domain/groupChannelSettings/module/moduleContext.js.map +1 -1
  53. package/lib/module/fragments/createGroupChannelListFragment.js +1 -1
  54. package/lib/module/fragments/createGroupChannelListFragment.js.map +1 -1
  55. package/lib/module/libs/SBUError.js +32 -0
  56. package/lib/module/libs/SBUError.js.map +1 -0
  57. package/lib/module/libs/SBUUtils.js +10 -0
  58. package/lib/module/libs/SBUUtils.js.map +1 -0
  59. package/lib/module/localization/StringSet.type.js +6 -0
  60. package/lib/module/localization/StringSet.type.js.map +1 -1
  61. package/lib/module/platform/createFileService.expo.js +5 -4
  62. package/lib/module/platform/createFileService.expo.js.map +1 -1
  63. package/lib/module/platform/createFileService.native.js +18 -8
  64. package/lib/module/platform/createFileService.native.js.map +1 -1
  65. package/lib/module/platform/types.js +1 -1
  66. package/lib/module/platform/types.js.map +1 -1
  67. package/lib/module/version.js +1 -1
  68. package/lib/module/version.js.map +1 -1
  69. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  70. package/lib/typescript/src/libs/SBUError.d.ts +14 -0
  71. package/lib/typescript/src/libs/SBUUtils.d.ts +3 -0
  72. package/lib/typescript/src/localization/StringSet.type.d.ts +3 -0
  73. package/lib/typescript/src/platform/types.d.ts +2 -1
  74. package/lib/typescript/src/version.d.ts +1 -1
  75. package/package.json +5 -5
  76. package/src/components/FileViewer.tsx +12 -4
  77. package/src/components/MessageRenderer/FileMessage/ImageFileMessage.tsx +55 -12
  78. package/src/components/MessageRenderer/FileMessage/VideoFileMessage.tsx +38 -30
  79. package/src/components/MessageRenderer/MessageIncomingSenderName.tsx +1 -1
  80. package/src/components/MessageRenderer/index.tsx +5 -2
  81. package/src/contexts/SendbirdChat.tsx +1 -1
  82. package/src/domain/groupChannel/component/GroupChannelInput/SendInput.tsx +28 -4
  83. package/src/domain/groupChannelSettings/component/GroupChannelSettingsInfo.tsx +1 -1
  84. package/src/domain/groupChannelSettings/module/moduleContext.tsx +26 -3
  85. package/src/fragments/createGroupChannelListFragment.tsx +1 -1
  86. package/src/libs/SBUError.ts +26 -0
  87. package/src/libs/SBUUtils.ts +9 -0
  88. package/src/localization/StringSet.type.ts +10 -0
  89. package/src/platform/createFileService.expo.ts +5 -4
  90. package/src/platform/createFileService.native.ts +17 -8
  91. package/src/platform/types.ts +3 -1
  92. package/src/version.ts +1 -1
@@ -1,10 +1,39 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Platform, StyleSheet, View } from 'react-native';
2
3
 
3
4
  import { Icon, Image, createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
4
- import { getAvailableUriFromFileMessage } from '@sendbird/uikit-utils';
5
+ import { getAvailableUriFromFileMessage, useForceUpdate } from '@sendbird/uikit-utils';
5
6
 
6
7
  import type { FileMessageProps } from './index';
7
8
 
9
+ const useRetry = (hasError: boolean, retryCount = 5) => {
10
+ if (Platform.OS === 'android') return '';
11
+
12
+ const forceUpdate = useForceUpdate();
13
+ const retryCountRef = useRef(1);
14
+ const retryTimeoutRef = useRef<NodeJS.Timeout>();
15
+
16
+ useEffect(() => {
17
+ if (hasError) {
18
+ const reloadReservation = () => {
19
+ if (retryCountRef.current < retryCount) {
20
+ retryTimeoutRef.current = setTimeout(() => {
21
+ retryCountRef.current++;
22
+ reloadReservation();
23
+ forceUpdate();
24
+ }, retryCountRef.current * 5000);
25
+ }
26
+ };
27
+
28
+ return reloadReservation();
29
+ } else {
30
+ return clearTimeout(retryTimeoutRef.current);
31
+ }
32
+ }, [hasError]);
33
+
34
+ return retryCountRef.current;
35
+ };
36
+
8
37
  const ImageFileMessage = ({ message }: FileMessageProps) => {
9
38
  const { colors } = useUIKitTheme();
10
39
  const [imageNotFound, setImageNotFound] = useState(false);
@@ -12,18 +41,28 @@ const ImageFileMessage = ({ message }: FileMessageProps) => {
12
41
  const fileUrl = getAvailableUriFromFileMessage(message);
13
42
  const style = [styles.image, { backgroundColor: colors.onBackground04 }];
14
43
 
15
- if (imageNotFound) {
16
- return <Icon containerStyle={style} icon={'thumbnail-none'} size={48} color={colors.onBackground02} />;
17
- }
44
+ const key = useRetry(imageNotFound);
18
45
 
19
46
  return (
20
- <Image
21
- source={{ uri: fileUrl }}
22
- style={style}
23
- resizeMode={'cover'}
24
- resizeMethod={'resize'}
25
- onError={() => setImageNotFound(true)}
26
- />
47
+ <View style={style}>
48
+ <Image
49
+ key={key}
50
+ source={{ uri: fileUrl }}
51
+ style={[StyleSheet.absoluteFill, imageNotFound && styles.hide]}
52
+ resizeMode={'cover'}
53
+ resizeMethod={'resize'}
54
+ onError={() => setImageNotFound(true)}
55
+ onLoad={() => setImageNotFound(false)}
56
+ />
57
+ {imageNotFound && (
58
+ <Icon
59
+ containerStyle={StyleSheet.absoluteFill}
60
+ icon={'thumbnail-none'}
61
+ size={48}
62
+ color={colors.onBackground02}
63
+ />
64
+ )}
65
+ </View>
27
66
  );
28
67
  };
29
68
 
@@ -33,6 +72,10 @@ const styles = createStyleSheet({
33
72
  maxWidth: 240,
34
73
  height: 160,
35
74
  borderRadius: 16,
75
+ overflow: 'hidden',
76
+ },
77
+ hide: {
78
+ display: 'none',
36
79
  },
37
80
  });
38
81
 
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import { View } from 'react-native';
3
3
 
4
4
  import { Icon, Image, createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
@@ -7,35 +7,49 @@ import { getAvailableUriFromFileMessage } from '@sendbird/uikit-utils';
7
7
  import { usePlatformService } from '../../../hooks/useContext';
8
8
  import type { FileMessageProps } from './index';
9
9
 
10
- const VideoFileMessage = ({ message }: FileMessageProps) => {
11
- const { colors } = useUIKitTheme();
10
+ const useRetry = (videoFileUrl: string, retryCount = 5) => {
11
+ const [state, setState] = useState({ thumbnail: null as null | string, loading: true });
12
+ const retryCountRef = useRef(0);
13
+ const retryTimeoutRef = useRef<NodeJS.Timeout>();
12
14
 
13
15
  const { mediaService } = usePlatformService();
14
- const fileUrl = getAvailableUriFromFileMessage(message);
15
- const style = [styles.image, { backgroundColor: colors.onBackground04 }];
16
16
 
17
- const [state, setState] = useState({
18
- thumbnail: null as null | string,
19
- loading: true,
20
- imageNotFound: false,
21
- });
17
+ const fetchThumbnail = () => {
18
+ return mediaService?.getVideoThumbnail({ url: videoFileUrl, timeMills: 1000 }).then((result) => {
19
+ setState({ loading: false, thumbnail: result?.path ?? null });
20
+ });
21
+ };
22
22
 
23
23
  useEffect(() => {
24
- mediaService
25
- ?.getVideoThumbnail({ url: fileUrl, timeMills: 1000 })
26
- .then((result) => {
27
- if (result?.path) {
28
- setState((prev) => ({ ...prev, loading: false, thumbnail: result.path }));
29
- } else {
30
- throw new Error('Cannot generate thumbnail');
24
+ if (!state.thumbnail) {
25
+ const reloadReservation = () => {
26
+ if (retryCountRef.current < retryCount) {
27
+ retryTimeoutRef.current = setTimeout(() => {
28
+ retryCountRef.current++;
29
+ reloadReservation();
30
+ fetchThumbnail();
31
+ }, retryCountRef.current * 5000);
31
32
  }
32
- })
33
- .catch(() => {
34
- setState((prev) => ({ ...prev, loading: false, imageNotFound: true }));
35
- });
36
- }, []);
33
+ };
34
+
35
+ return reloadReservation();
36
+ } else {
37
+ return clearTimeout(retryTimeoutRef.current);
38
+ }
39
+ }, [state.thumbnail]);
40
+
41
+ return state;
42
+ };
43
+
44
+ const VideoFileMessage = ({ message }: FileMessageProps) => {
45
+ const { colors } = useUIKitTheme();
46
+
47
+ const fileUrl = getAvailableUriFromFileMessage(message);
48
+ const style = [styles.image, { backgroundColor: colors.onBackground04 }];
49
+
50
+ const { loading, thumbnail } = useRetry(fileUrl);
37
51
 
38
- if (state.loading || state.imageNotFound) {
52
+ if (loading) {
39
53
  return (
40
54
  <View style={[style, styles.container]}>
41
55
  <PlayIcon />
@@ -45,13 +59,7 @@ const VideoFileMessage = ({ message }: FileMessageProps) => {
45
59
 
46
60
  return (
47
61
  <View style={styles.container}>
48
- <Image
49
- source={{ uri: state.thumbnail || fileUrl }}
50
- style={style}
51
- resizeMode={'cover'}
52
- resizeMethod={'resize'}
53
- onError={() => setState((prev) => ({ ...prev, imageNotFound: true }))}
54
- />
62
+ <Image source={{ uri: thumbnail || fileUrl }} style={style} resizeMode={'cover'} resizeMethod={'resize'} />
55
63
  <PlayIcon />
56
64
  </View>
57
65
  );
@@ -18,7 +18,7 @@ const MessageIncomingSenderName = ({ message, grouping }: Props) => {
18
18
  return (
19
19
  <View style={styles.sender}>
20
20
  {(message.isFileMessage() || message.isUserMessage()) && (
21
- <Text caption1 color={colors.ui.message.incoming.enabled.textSenderName}>
21
+ <Text caption1 color={colors.ui.message.incoming.enabled.textSenderName} numberOfLines={1}>
22
22
  {message.sender?.nickname || STRINGS.LABELS.USER_NO_NAME}
23
23
  </Text>
24
24
  )}
@@ -107,9 +107,9 @@ const MessageRenderer: GroupChannelProps['Fragment']['renderMessage'] = ({
107
107
  </View>
108
108
  )}
109
109
  {isIncoming && <MessageIncomingAvatar message={message} grouping={groupWithNext} />}
110
- <View>
110
+ <View style={styles.bubbleContainer}>
111
111
  {isIncoming && <MessageIncomingSenderName message={message} grouping={groupWithPrev} />}
112
- <View style={styles.bubbleContainer}>
112
+ <View style={styles.bubbleWrapper}>
113
113
  {messageComponent}
114
114
  {isIncoming && <MessageTime message={message} grouping={groupWithNext} style={styles.timeIncoming} />}
115
115
  </View>
@@ -150,6 +150,9 @@ const styles = createStyleSheet({
150
150
  maxWidth: 240,
151
151
  },
152
152
  bubbleContainer: {
153
+ flexShrink: 1,
154
+ },
155
+ bubbleWrapper: {
153
156
  flexDirection: 'row',
154
157
  alignItems: 'flex-end',
155
158
  },
@@ -91,7 +91,7 @@ export const SendbirdChatProvider = ({
91
91
  const listener = (status: AppStateStatus) => {
92
92
  // 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
93
93
  if (status === 'active') sdkInstance.getConnectionState() === 'CLOSED' && sdkInstance.setForegroundState();
94
- else sdkInstance.getConnectionState() === 'OPEN' && sdkInstance.setBackgroundState();
94
+ else if (status === 'background') sdkInstance.getConnectionState() === 'OPEN' && sdkInstance.setBackgroundState();
95
95
  };
96
96
 
97
97
  const subscriber = AppState.addEventListener('change', listener);
@@ -5,6 +5,7 @@ import {
5
5
  Icon,
6
6
  TextInput,
7
7
  createStyleSheet,
8
+ useAlert,
8
9
  useBottomSheet,
9
10
  useToast,
10
11
  useUIKitTheme,
@@ -12,6 +13,8 @@ import {
12
13
  import { conditionChaining } from '@sendbird/uikit-utils';
13
14
 
14
15
  import { useLocalization, usePlatformService } from '../../../../hooks/useContext';
16
+ import SBUError from '../../../../libs/SBUError';
17
+ import SBUUtils from '../../../../libs/SBUUtils';
15
18
  import type { GroupChannelProps } from '../../types';
16
19
 
17
20
  type SendInputProps = GroupChannelProps['Input'] & {
@@ -21,9 +24,10 @@ type SendInputProps = GroupChannelProps['Input'] & {
21
24
  };
22
25
  const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabled }: SendInputProps) => {
23
26
  const { STRINGS } = useLocalization();
24
- const { openSheet } = useBottomSheet();
25
27
  const { fileService } = usePlatformService();
26
28
  const { colors } = useUIKitTheme();
29
+ const { openSheet } = useBottomSheet();
30
+ const { alert } = useAlert();
27
31
  const toast = useToast();
28
32
 
29
33
  const onPressSend = () => {
@@ -39,7 +43,17 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
39
43
  onPress: async () => {
40
44
  const photo = await fileService.openCamera({
41
45
  mediaType: 'all',
42
- onOpenFailureWithToastMessage: () => toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error'),
46
+ onOpenFailure: (error) => {
47
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
48
+ alert({
49
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
50
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE('camera', 'UIKitSample'),
51
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
52
+ });
53
+ } else {
54
+ toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
55
+ }
56
+ },
43
57
  });
44
58
 
45
59
  if (photo) {
@@ -54,7 +68,17 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
54
68
  const photo = await fileService.openMediaLibrary({
55
69
  selectionLimit: 1,
56
70
  mediaType: 'all',
57
- onOpenFailureWithToastMessage: () => toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error'),
71
+ onOpenFailure: (error) => {
72
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
73
+ alert({
74
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
75
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE('device storage', 'UIKitSample'),
76
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
77
+ });
78
+ } else {
79
+ toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error');
80
+ }
81
+ },
58
82
  });
59
83
 
60
84
  if (photo && photo[0]) {
@@ -67,7 +91,7 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
67
91
  icon: 'document',
68
92
  onPress: async () => {
69
93
  const file = await fileService.openDocument({
70
- onOpenFailureWithToastMessage: () => toast.show(STRINGS.TOAST.OPEN_FILES_ERROR, 'error'),
94
+ onOpenFailure: () => toast.show(STRINGS.TOAST.OPEN_FILES_ERROR, 'error'),
71
95
  });
72
96
 
73
97
  if (file) {
@@ -15,7 +15,7 @@ const GroupChannelSettingsInfo = (_: GroupChannelSettingsProps['Info']) => {
15
15
  const { STRINGS } = useLocalization();
16
16
 
17
17
  if (!currentUser) {
18
- Logger.warn('Cannot render GroupChannelSettingsInfo, User is not connected');
18
+ Logger.warn('Cannot render GroupChannelSettingsInfo, please connect using `useConnection()` hook first');
19
19
  return null;
20
20
  }
21
21
 
@@ -1,7 +1,7 @@
1
1
  import React, { createContext, useCallback } from 'react';
2
2
 
3
3
  import { useActiveGroupChannel, useChannelHandler } from '@sendbird/uikit-chat-hooks';
4
- import { useActionMenu, useBottomSheet, usePrompt, useToast } from '@sendbird/uikit-react-native-foundation';
4
+ import { useActionMenu, useAlert, useBottomSheet, usePrompt, useToast } from '@sendbird/uikit-react-native-foundation';
5
5
  import {
6
6
  NOOP,
7
7
  SendbirdGroupChannel,
@@ -14,6 +14,8 @@ import {
14
14
 
15
15
  import ProviderLayout from '../../../components/ProviderLayout';
16
16
  import { useLocalization, usePlatformService, useSendbirdChat } from '../../../hooks/useContext';
17
+ import SBUError from '../../../libs/SBUError';
18
+ import SBUUtils from '../../../libs/SBUUtils';
17
19
  import type { GroupChannelSettingsContextsType, GroupChannelSettingsModule } from '../types';
18
20
 
19
21
  export const GroupChannelSettingsContexts: GroupChannelSettingsContextsType = {
@@ -32,6 +34,7 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
32
34
  const { STRINGS } = useLocalization();
33
35
  const { sdk } = useSendbirdChat();
34
36
  const { fileService } = usePlatformService();
37
+ const { alert } = useAlert();
35
38
 
36
39
  const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
37
40
 
@@ -84,7 +87,17 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
84
87
  onPress: async () => {
85
88
  const file = await fileService.openCamera({
86
89
  mediaType: 'photo',
87
- onOpenFailureWithToastMessage: () => toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error'),
90
+ onOpenFailure: (error) => {
91
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
92
+ alert({
93
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
94
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE('camera', 'UIKitSample'),
95
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
96
+ });
97
+ } else {
98
+ toast.show(STRINGS.TOAST.OPEN_CAMERA_ERROR, 'error');
99
+ }
100
+ },
88
101
  });
89
102
  if (!file) return;
90
103
 
@@ -99,7 +112,17 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
99
112
  const files = await fileService.openMediaLibrary({
100
113
  selectionLimit: 1,
101
114
  mediaType: 'photo',
102
- onOpenFailureWithToastMessage: () => toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error'),
115
+ onOpenFailure: (error) => {
116
+ if (error.code === SBUError.CODE.ERR_PERMISSIONS_DENIED) {
117
+ alert({
118
+ title: STRINGS.DIALOG.ALERT_PERMISSIONS_TITLE,
119
+ message: STRINGS.DIALOG.ALERT_PERMISSIONS_MESSAGE('device storage', 'UIKitSample'),
120
+ buttons: [{ text: STRINGS.DIALOG.ALERT_PERMISSIONS_OK, onPress: () => SBUUtils.openSettings() }],
121
+ });
122
+ } else {
123
+ toast.show(STRINGS.TOAST.OPEN_PHOTO_LIBRARY_ERROR, 'error');
124
+ }
125
+ },
103
126
  });
104
127
  if (!files || !files[0]) return;
105
128
 
@@ -57,7 +57,7 @@ const createGroupChannelListFragment = (initModule?: Partial<GroupChannelListMod
57
57
  );
58
58
 
59
59
  if (!currentUser) {
60
- Logger.warn('Cannot render GroupChannelListFragment, User is not connected');
60
+ Logger.warn('Cannot render GroupChannelListFragment, please connect using `useConnection()` hook first');
61
61
  return null;
62
62
  }
63
63
 
@@ -0,0 +1,26 @@
1
+ enum SBUErrorCode {
2
+ ERR_UNKNOWN = 90000000,
3
+
4
+ // Platform service - 91001000 ~
5
+ ERR_PERMISSIONS_DENIED = 91001000,
6
+ ERR_DEVICE_UNAVAILABLE,
7
+ }
8
+
9
+ export default class SBUError extends Error {
10
+ static CODE = SBUErrorCode;
11
+
12
+ static get UNKNOWN() {
13
+ return new SBUError(SBUErrorCode.ERR_UNKNOWN);
14
+ }
15
+
16
+ static get PERMISSIONS_DENIED() {
17
+ return new SBUError(SBUErrorCode.ERR_PERMISSIONS_DENIED);
18
+ }
19
+ static get DEVICE_UNAVAILABLE() {
20
+ return new SBUError(SBUErrorCode.ERR_DEVICE_UNAVAILABLE);
21
+ }
22
+
23
+ constructor(public code: SBUErrorCode, message?: string) {
24
+ super(message);
25
+ }
26
+ }
@@ -0,0 +1,9 @@
1
+ import { Linking, Platform } from 'react-native';
2
+
3
+ export default class SBUUtils {
4
+ static openSettings() {
5
+ Linking.openSettings().catch(() => {
6
+ if (Platform.OS === 'ios') Linking.openURL('App-Prefs:root');
7
+ });
8
+ }
9
+ }
@@ -149,6 +149,11 @@ export interface StringSet {
149
149
  };
150
150
  DIALOG: {
151
151
  ALERT_DEFAULT_OK: string;
152
+
153
+ ALERT_PERMISSIONS_TITLE: string;
154
+ ALERT_PERMISSIONS_MESSAGE: (permission: string, appName: string) => string;
155
+ ALERT_PERMISSIONS_OK: string;
156
+
152
157
  PROMPT_DEFAULT_OK: string;
153
158
  PROMPT_DEFAULT_CANCEL: string;
154
159
  PROMPT_DEFAULT_PLACEHOLDER: string;
@@ -316,6 +321,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
316
321
  },
317
322
  DIALOG: {
318
323
  ALERT_DEFAULT_OK: 'OK',
324
+ ALERT_PERMISSIONS_TITLE: 'Allow permission',
325
+ ALERT_PERMISSIONS_MESSAGE: (permission, appName = 'Application') => {
326
+ return `${appName} need permission to access your ${permission}. Go to Settings to allow access`;
327
+ },
328
+ ALERT_PERMISSIONS_OK: 'SETTINGS',
319
329
  PROMPT_DEFAULT_OK: 'Submit',
320
330
  PROMPT_DEFAULT_CANCEL: 'Cancel',
321
331
  PROMPT_DEFAULT_PLACEHOLDER: 'Enter',
@@ -5,6 +5,7 @@ import type * as ExpoMediaLibrary from 'expo-media-library';
5
5
 
6
6
  import { getFileExtension, getFileType } from '@sendbird/uikit-utils';
7
7
 
8
+ import SBUError from '../libs/SBUError';
8
9
  import type { ExpoMediaLibraryPermissionResponse, ExpoPermissionResponse } from '../utils/expoPermissionGranted';
9
10
  import expoPermissionGranted from '../utils/expoPermissionGranted';
10
11
  import fileTypeGuard from '../utils/fileTypeGuard';
@@ -55,7 +56,7 @@ const createExpoFileService = ({
55
56
  if (!hasPermission) {
56
57
  const granted = await this.requestCameraPermission();
57
58
  if (!granted) {
58
- options?.onOpenFailureWithToastMessage?.();
59
+ options?.onOpenFailure?.(SBUError.PERMISSIONS_DENIED);
59
60
  return null;
60
61
  }
61
62
  }
@@ -89,7 +90,7 @@ const createExpoFileService = ({
89
90
  if (!hasPermission) {
90
91
  const granted = await this.requestMediaLibraryPermission('read');
91
92
  if (!granted) {
92
- options?.onOpenFailureWithToastMessage?.();
93
+ options?.onOpenFailure?.(SBUError.PERMISSIONS_DENIED);
93
94
  return null;
94
95
  }
95
96
  }
@@ -123,8 +124,8 @@ const createExpoFileService = ({
123
124
  if (response.type === 'cancel') return null;
124
125
  const { mimeType, uri, size, name } = response;
125
126
  return fileTypeGuard({ uri, size, name, type: mimeType });
126
- } catch {
127
- options?.onOpenFailureWithToastMessage?.();
127
+ } catch (e) {
128
+ options?.onOpenFailure?.(SBUError.UNKNOWN, e);
128
129
  return null;
129
130
  }
130
131
  }
@@ -7,7 +7,9 @@ import type * as Permissions from 'react-native-permissions';
7
7
  import type { Permission } from 'react-native-permissions';
8
8
 
9
9
  import { getFileExtension, getFileType } from '@sendbird/uikit-utils';
10
+ import { normalizeFileName } from '@sendbird/uikit-utils/src/shared/regex';
10
11
 
12
+ import SBUError from '../libs/SBUError';
11
13
  import fileTypeGuard from '../utils/fileTypeGuard';
12
14
  import nativePermissionGranted from '../utils/nativePermissionGranted';
13
15
  import type {
@@ -87,12 +89,13 @@ const createNativeFileService = ({
87
89
  if (!hasPermission) {
88
90
  const granted = await this.requestCameraPermission();
89
91
  if (!granted) {
90
- options?.onOpenFailureWithToastMessage?.();
92
+ options?.onOpenFailure?.(SBUError.PERMISSIONS_DENIED);
91
93
  return null;
92
94
  }
93
95
  }
94
96
 
95
97
  const response = await imagePickerModule.launchCamera({
98
+ presentationStyle: 'fullScreen',
96
99
  cameraType: options?.cameraType ?? 'back',
97
100
  mediaType: (() => {
98
101
  switch (options?.mediaType) {
@@ -109,7 +112,7 @@ const createNativeFileService = ({
109
112
  });
110
113
  if (response.didCancel) return null;
111
114
  if (response.errorCode === 'camera_unavailable') {
112
- options?.onOpenFailureWithToastMessage?.();
115
+ options?.onOpenFailure?.(SBUError.DEVICE_UNAVAILABLE, new Error(response.errorMessage));
113
116
  return null;
114
117
  }
115
118
 
@@ -126,12 +129,13 @@ const createNativeFileService = ({
126
129
  if (!hasPermission) {
127
130
  const granted = await this.requestMediaLibraryPermission();
128
131
  if (!granted) {
129
- options?.onOpenFailureWithToastMessage?.();
132
+ options?.onOpenFailure?.(SBUError.PERMISSIONS_DENIED);
130
133
  return null;
131
134
  }
132
135
  }
133
136
 
134
137
  const response = await imagePickerModule.launchImageLibrary({
138
+ presentationStyle: 'fullScreen',
135
139
  selectionLimit,
136
140
  mediaType: (() => {
137
141
  switch (options?.mediaType) {
@@ -148,7 +152,7 @@ const createNativeFileService = ({
148
152
  });
149
153
  if (response.didCancel) return null;
150
154
  if (response.errorCode === 'camera_unavailable') {
151
- options?.onOpenFailureWithToastMessage?.();
155
+ options?.onOpenFailure?.(SBUError.DEVICE_UNAVAILABLE, new Error(response.errorMessage));
152
156
  return null;
153
157
  }
154
158
 
@@ -162,7 +166,7 @@ const createNativeFileService = ({
162
166
  return fileTypeGuard({ uri, size, name, type });
163
167
  } catch (e) {
164
168
  if (!documentPickerModule.isCancel(e) && documentPickerModule.isInProgress(e)) {
165
- options?.onOpenFailureWithToastMessage?.();
169
+ options?.onOpenFailure?.(SBUError.UNKNOWN, e);
166
170
  }
167
171
  return null;
168
172
  }
@@ -186,13 +190,18 @@ const createNativeFileService = ({
186
190
  await fsModule.FileSystem.fetch(options.fileUrl, { path: downloadPath });
187
191
  const fileType = getFileType(getFileExtension(options.fileUrl));
188
192
 
189
- if (Platform.OS === 'ios' && fileType.match(/image|video/)) {
190
- await mediaLibraryModule.save(downloadPath);
193
+ if (Platform.OS === 'ios' && (fileType === 'image' || fileType === 'video')) {
194
+ const type = ({ 'image': 'photo', 'video': 'video' } as const)[fileType];
195
+ await mediaLibraryModule.save(downloadPath, { type });
191
196
  }
192
197
 
193
198
  if (Platform.OS === 'android') {
194
199
  const dirType = { 'file': 'downloads', 'audio': 'audio', 'image': 'images', 'video': 'video' } as const;
195
- await fsModule.FileSystem.cpExternal(downloadPath, options.fileName, dirType[fileType]);
200
+ await fsModule.FileSystem.cpExternal(
201
+ downloadPath,
202
+ normalizeFileName(options.fileName, getFileExtension(options.fileUrl)),
203
+ dirType[fileType],
204
+ );
196
205
  }
197
206
  return downloadPath;
198
207
  }
@@ -1,3 +1,5 @@
1
+ import type SBUError from '../libs/SBUError';
2
+
1
3
  export type Unsubscribe = () => void | undefined;
2
4
  export type DownloadedPath = string;
3
5
  export type FilePickerResponse = FileType | null;
@@ -23,7 +25,7 @@ export interface ClipboardServiceInterface {
23
25
  export interface FileServiceInterface extends FilePickerServiceInterface, FileSystemServiceInterface {}
24
26
 
25
27
  export interface OpenResultListener {
26
- onOpenFailureWithToastMessage?: () => void;
28
+ onOpenFailure?: (error: SBUError, originError?: unknown) => void;
27
29
  }
28
30
  export interface OpenMediaLibraryOptions extends OpenResultListener {
29
31
  selectionLimit?: number;
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- const VERSION = '1.1.1';
1
+ const VERSION = '1.1.3';
2
2
  export default VERSION;