Skip to content

Commit aed36a4

Browse files
authored
Merge pull request #39 from sendbird/fix/QM-1763
[QM-1763]Fix/video file message, file viewer
2 parents a369813 + 255fcfb commit aed36a4

File tree

16 files changed

+238
-42
lines changed

16 files changed

+238
-42
lines changed

packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithCollection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const createGroupChannelListCollection = (
1919

2020
const defaultOptions = {
2121
includeEmpty: false,
22-
limit: 5,
22+
limit: 20,
2323
order: sdk.GroupChannelCollection.GroupChannelOrder.LATEST_LAST_MESSAGE,
2424
};
2525
const collectionBuilder = sdk.GroupChannel.createGroupChannelCollection();

packages/uikit-react-native/src/components/FileViewer.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ import {
1414
useUIKitTheme,
1515
} from '@sendbird/uikit-react-native-foundation';
1616
import type { SendbirdFileMessage } from '@sendbird/uikit-utils';
17-
import { Logger, getFileExtension, getFileType, isMyMessage, toMegabyte, useIIFE } from '@sendbird/uikit-utils';
17+
import {
18+
Logger,
19+
getFileExtension,
20+
getFileType,
21+
isMyMessage,
22+
toMegabyte,
23+
truncate,
24+
useIIFE,
25+
} from '@sendbird/uikit-utils';
1826

1927
import { useLocalization, usePlatformService, useSendbirdChat } from '../hooks/useContext';
2028

@@ -194,10 +202,10 @@ const FileViewerHeader = ({ topInset, onClose, subtitle, title }: HeaderProps) =
194202
<Icon icon={'close'} size={24} color={palette.onBackgroundDark01} />
195203
</TouchableOpacity>
196204
<View style={styles.barTitleContainer}>
197-
<Text h2 color={palette.onBackgroundDark01} style={styles.headerTitle}>
198-
{title}
205+
<Text h2 color={palette.onBackgroundDark01} style={styles.headerTitle} numberOfLines={1}>
206+
{truncate(title, { mode: 'mid', maxLen: 18 })}
199207
</Text>
200-
<Text caption2 color={palette.onBackgroundDark01}>
208+
<Text caption2 color={palette.onBackgroundDark01} numberOfLines={1}>
201209
{subtitle}
202210
</Text>
203211
</View>

packages/uikit-react-native/src/components/MessageRenderer/FileMessage/ImageFileMessage.tsx

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,68 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { Platform, StyleSheet, View } from 'react-native';
23

34
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';
56

67
import type { FileMessageProps } from './index';
78

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+
clearTimeout(retryTimeoutRef.current);
31+
}
32+
}, [hasError]);
33+
34+
return retryCountRef.current;
35+
};
36+
837
const ImageFileMessage = ({ message }: FileMessageProps) => {
938
const { colors } = useUIKitTheme();
1039
const [imageNotFound, setImageNotFound] = useState(false);
1140

1241
const fileUrl = getAvailableUriFromFileMessage(message);
1342
const style = [styles.image, { backgroundColor: colors.onBackground04 }];
1443

15-
if (imageNotFound) {
16-
return <Icon containerStyle={style} icon={'thumbnail-none'} size={48} color={colors.onBackground02} />;
17-
}
44+
const key = useRetry(imageNotFound);
1845

1946
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>
2766
);
2867
};
2968

@@ -33,6 +72,10 @@ const styles = createStyleSheet({
3372
maxWidth: 240,
3473
height: 160,
3574
borderRadius: 16,
75+
overflow: 'hidden',
76+
},
77+
hide: {
78+
display: 'none',
3679
},
3780
});
3881

packages/uikit-react-native/src/components/MessageRenderer/MessageIncomingSenderName.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const MessageIncomingSenderName = ({ message, grouping }: Props) => {
1818
return (
1919
<View style={styles.sender}>
2020
{(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}>
2222
{message.sender?.nickname || STRINGS.LABELS.USER_NO_NAME}
2323
</Text>
2424
)}

packages/uikit-react-native/src/components/MessageRenderer/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ const MessageRenderer: GroupChannelProps['Fragment']['renderMessage'] = ({
107107
</View>
108108
)}
109109
{isIncoming && <MessageIncomingAvatar message={message} grouping={groupWithNext} />}
110-
<View>
110+
<View style={styles.bubbleContainer}>
111111
{isIncoming && <MessageIncomingSenderName message={message} grouping={groupWithPrev} />}
112-
<View style={styles.bubbleContainer}>
112+
<View style={styles.bubbleWrapper}>
113113
{messageComponent}
114114
{isIncoming && <MessageTime message={message} grouping={groupWithNext} style={styles.timeIncoming} />}
115115
</View>
@@ -150,6 +150,9 @@ const styles = createStyleSheet({
150150
maxWidth: 240,
151151
},
152152
bubbleContainer: {
153+
flexShrink: 1,
154+
},
155+
bubbleWrapper: {
153156
flexDirection: 'row',
154157
alignItems: 'flex-end',
155158
},

packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelInput/SendInput.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
Icon,
66
TextInput,
77
createStyleSheet,
8+
useAlert,
89
useBottomSheet,
910
useToast,
1011
useUIKitTheme,
1112
} from '@sendbird/uikit-react-native-foundation';
1213
import { conditionChaining } from '@sendbird/uikit-utils';
1314

1415
import { useLocalization, usePlatformService } from '../../../../hooks/useContext';
16+
import SBUError from '../../../../libs/SBUError';
17+
import SBUUtils from '../../../../libs/SBUUtils';
1518
import type { GroupChannelProps } from '../../types';
1619

1720
type SendInputProps = GroupChannelProps['Input'] & {
@@ -21,9 +24,10 @@ type SendInputProps = GroupChannelProps['Input'] & {
2124
};
2225
const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabled }: SendInputProps) => {
2326
const { STRINGS } = useLocalization();
24-
const { openSheet } = useBottomSheet();
2527
const { fileService } = usePlatformService();
2628
const { colors } = useUIKitTheme();
29+
const { openSheet } = useBottomSheet();
30+
const { alert } = useAlert();
2731
const toast = useToast();
2832

2933
const onPressSend = () => {
@@ -39,7 +43,17 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
3943
onPress: async () => {
4044
const photo = await fileService.openCamera({
4145
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+
},
4357
});
4458

4559
if (photo) {
@@ -54,7 +68,17 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
5468
const photo = await fileService.openMediaLibrary({
5569
selectionLimit: 1,
5670
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+
},
5882
});
5983

6084
if (photo && photo[0]) {
@@ -67,7 +91,7 @@ const SendInput = ({ onSendUserMessage, onSendFileMessage, text, setText, disabl
6791
icon: 'document',
6892
onPress: async () => {
6993
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'),
7195
});
7296

7397
if (file) {

packages/uikit-react-native/src/domain/groupChannelSettings/module/moduleContext.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { createContext, useCallback } from 'react';
22

33
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';
55
import {
66
NOOP,
77
SendbirdGroupChannel,
@@ -14,6 +14,8 @@ import {
1414

1515
import ProviderLayout from '../../../components/ProviderLayout';
1616
import { useLocalization, usePlatformService, useSendbirdChat } from '../../../hooks/useContext';
17+
import SBUError from '../../../libs/SBUError';
18+
import SBUUtils from '../../../libs/SBUUtils';
1719
import type { GroupChannelSettingsContextsType, GroupChannelSettingsModule } from '../types';
1820

1921
export const GroupChannelSettingsContexts: GroupChannelSettingsContextsType = {
@@ -32,6 +34,7 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
3234
const { STRINGS } = useLocalization();
3335
const { sdk } = useSendbirdChat();
3436
const { fileService } = usePlatformService();
37+
const { alert } = useAlert();
3538

3639
const { activeChannel, setActiveChannel } = useActiveGroupChannel(sdk, channel);
3740

@@ -84,7 +87,17 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
8487
onPress: async () => {
8588
const file = await fileService.openCamera({
8689
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+
},
88101
});
89102
if (!file) return;
90103

@@ -99,7 +112,17 @@ export const GroupChannelSettingsContextsProvider: GroupChannelSettingsModule['P
99112
const files = await fileService.openMediaLibrary({
100113
selectionLimit: 1,
101114
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+
},
103126
});
104127
if (!files || !files[0]) return;
105128

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

packages/uikit-react-native/src/localization/StringSet.type.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ export interface StringSet {
149149
};
150150
DIALOG: {
151151
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+
152157
PROMPT_DEFAULT_OK: string;
153158
PROMPT_DEFAULT_CANCEL: string;
154159
PROMPT_DEFAULT_PLACEHOLDER: string;
@@ -316,6 +321,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
316321
},
317322
DIALOG: {
318323
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',
319329
PROMPT_DEFAULT_OK: 'Submit',
320330
PROMPT_DEFAULT_CANCEL: 'Cancel',
321331
PROMPT_DEFAULT_PLACEHOLDER: 'Enter',

0 commit comments

Comments
 (0)