Skip to content

Commit

Permalink
feat(uikit): added file viewer component
Browse files Browse the repository at this point in the history
  • Loading branch information
bang9 committed Aug 31, 2022
1 parent 70da6f1 commit 9b9d52b
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { StatusBar } from 'react-native';
import { StatusBar, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import type { BaseHeaderProps, HeaderElement } from '../types';
import getDefaultHeaderHeight from './getDefaultHeaderHeight';

export type HeaderStyleContextType = {
HeaderComponent: (
Expand All @@ -20,12 +21,14 @@ export type HeaderStyleContextType = {
defaultTitleAlign: 'left' | 'center';
statusBarTranslucent: boolean;
topInset: number;
defaultHeight: number;
};
export const HeaderStyleContext = React.createContext<HeaderStyleContextType>({
HeaderComponent: () => null,
defaultTitleAlign: 'left',
statusBarTranslucent: true,
topInset: StatusBar.currentHeight ?? 0,
defaultHeight: getDefaultHeaderHeight(false),
});

type Props = Pick<HeaderStyleContextType, 'statusBarTranslucent' | 'defaultTitleAlign' | 'HeaderComponent'>;
Expand All @@ -36,6 +39,7 @@ export const HeaderStyleProvider = ({
statusBarTranslucent,
}: React.PropsWithChildren<Props>) => {
const { top } = useSafeAreaInsets();
const { width, height } = useWindowDimensions();

return (
<HeaderStyleContext.Provider
Expand All @@ -44,6 +48,7 @@ export const HeaderStyleProvider = ({
defaultTitleAlign,
statusBarTranslucent,
topInset: statusBarTranslucent ? top : 0,
defaultHeight: getDefaultHeaderHeight(width > height),
}}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react';
import { TouchableOpacity, TouchableOpacityProps, View, useWindowDimensions } from 'react-native';
import { TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { conditionChaining } from '@sendbird/uikit-utils';

import type { BaseHeaderProps } from '../../index';
import createStyleSheet from '../../styles/createStyleSheet';
import getDefaultHeaderHeight from '../../styles/getDefaultHeaderHeight';
import useHeaderStyle from '../../styles/useHeaderStyle';
import useUIKitTheme from '../../theme/useUIKitTheme';
import Text, { TextProps } from '../Text';
Expand Down Expand Up @@ -38,8 +37,7 @@ const Header: ((props: HeaderProps) => JSX.Element) & {
onPressRight,
clearTitleMargin = false,
}) => {
const { topInset, defaultTitleAlign } = useHeaderStyle();
const { width, height } = useWindowDimensions();
const { topInset, defaultTitleAlign, defaultHeight } = useHeaderStyle();
const { colors } = useUIKitTheme();
const { left: paddingLeft, right: paddingRight } = useSafeAreaInsets();

Expand All @@ -64,7 +62,7 @@ const Header: ((props: HeaderProps) => JSX.Element) & {
},
]}
>
<View style={[styles.header, { height: getDefaultHeaderHeight(width > height) }]}>
<View style={[styles.header, { height: defaultHeight }]}>
<LeftSide titleAlign={actualTitleAlign} left={left} onPressLeft={onPressLeft} />
<View
style={[
Expand Down
1 change: 1 addition & 0 deletions packages/uikit-react-native/src/SendbirdUIKitContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type SendbirdUIKitContainerProps = React.PropsWithChildren<{
theme?: UIKitTheme;
statusBarTranslucent?: boolean;
defaultHeaderTitleAlign?: 'left' | 'center';
defaultHeaderHeight?: number;
HeaderComponent?: HeaderStyleContextType['HeaderComponent'];
};
toast?: {
Expand Down
284 changes: 284 additions & 0 deletions packages/uikit-react-native/src/components/FileViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import React, { useEffect, useState } from 'react';
import { StatusBar, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import {
Icon,
Image,
LoadingSpinner,
Text,
createStyleSheet,
useAlert,
useHeaderStyle,
useToast,
useUIKitTheme,
} from '@sendbird/uikit-react-native-foundation';
import type { SendbirdFileMessage } from '@sendbird/uikit-utils';
import { Logger, getFileExtension, getFileType, isMyMessage, toMegabyte, useIIFE } from '@sendbird/uikit-utils';

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

type Props = {
fileMessage: SendbirdFileMessage;
deleteMessage: () => Promise<void>;

onClose: () => void;
onPressDownload?: (message: SendbirdFileMessage) => void;
onPressDelete?: (message: SendbirdFileMessage) => void;

headerShown?: boolean;
headerTopInset?: number;
};
const FileViewer = ({
headerShown = true,
deleteMessage,
headerTopInset,
fileMessage,
onPressDownload,
onPressDelete,
onClose,
}: Props) => {
const [loading, setLoading] = useState(true);

const { bottom } = useSafeAreaInsets();

const { currentUser } = useSendbirdChat();
const { palette } = useUIKitTheme();
const { topInset, statusBarTranslucent, defaultHeight } = useHeaderStyle();
const { STRINGS } = useLocalization();
const { fileService, mediaService } = usePlatformService();
const toast = useToast();
const { alert } = useAlert();

const basicTopInset = statusBarTranslucent ? topInset : 0;
const canDelete = isMyMessage(fileMessage, currentUser?.userId);
const fileType = getFileType(fileMessage.type || getFileExtension(fileMessage.url));

useEffect(() => {
if (!mediaService?.VideoComponent || fileType === 'file') {
onClose();
}
}, [mediaService]);

const fileViewer = useIIFE(() => {
switch (fileType) {
case 'image': {
return (
<Image
source={{ uri: fileMessage.url }}
style={StyleSheet.absoluteFill}
resizeMode={'contain'}
onLoadEnd={() => setLoading(false)}
/>
);
}

case 'video':
case 'audio': {
if (!mediaService?.VideoComponent) return null;
return (
<mediaService.VideoComponent
source={{ uri: fileMessage.url }}
style={[StyleSheet.absoluteFill, { top: basicTopInset + defaultHeight, bottom: defaultHeight + bottom }]}
resizeMode={'contain'}
onLoad={() => setLoading(false)}
/>
);
}

default: {
return null;
}
}
});

const _onPressDelete = () => {
if (!canDelete) return;

if (onPressDelete) {
onPressDelete(fileMessage);
} else {
alert({
title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_TITLE,
buttons: [
{
text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL,
},
{
text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_OK,
style: 'destructive',
onPress: () => {
deleteMessage()
.then(() => {
onClose();
})
.catch(() => {
toast.show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error');
});
},
},
],
});
}
};

const _onPressDownload = () => {
if (onPressDownload) {
onPressDownload(fileMessage);
} else {
if (toMegabyte(fileMessage.size) > 4) {
toast.show(STRINGS.TOAST.DOWNLOAD_START, 'success');
}

fileService
.save({ fileUrl: fileMessage.url, fileName: fileMessage.name, fileType: fileMessage.type })
.then((response) => {
toast.show(STRINGS.TOAST.DOWNLOAD_OK, 'success');
Logger.log('File saved to', response);
})
.catch((err) => {
toast.show(STRINGS.TOAST.DOWNLOAD_ERROR, 'error');
Logger.log('File save failure', err);
});
}
};

return (
<View style={{ flex: 1, backgroundColor: palette.background700 }}>
<StatusBar barStyle={'light-content'} animated />
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
{fileViewer}
{loading && <LoadingSpinner style={{ position: 'absolute' }} size={40} color={palette.primary300} />}
</View>
{headerShown && (
<FileViewerHeader
title={STRINGS.FILE_VIEWER.TITLE(fileMessage)}
subtitle={STRINGS.FILE_VIEWER.SUBTITLE(fileMessage)}
topInset={headerTopInset ?? basicTopInset}
onClose={onClose}
/>
)}
<FileViewerFooter
bottomInset={bottom}
deleteShown={canDelete}
onPressDelete={_onPressDelete}
onPressDownload={_onPressDownload}
/>
</View>
);
};

type HeaderProps = {
topInset: number;
onClose: () => void;
title: string;
subtitle: string;
};
const FileViewerHeader = ({ topInset, onClose, subtitle, title }: HeaderProps) => {
const { palette } = useUIKitTheme();
const { defaultHeight } = useHeaderStyle();
const { left, right } = useSafeAreaInsets();

return (
<View
style={[
styles.headerContainer,
{
paddingLeft: styles.headerContainer.paddingHorizontal + left,
paddingRight: styles.headerContainer.paddingHorizontal + right,
},
{ paddingTop: topInset, height: defaultHeight + topInset, backgroundColor: palette.overlay01 },
]}
>
<SBUPressable as={'TouchableOpacity'} onPress={onClose} style={styles.barButton}>
<Icon icon={'close'} size={24} color={palette.onBackgroundDark01} />
</SBUPressable>
<View style={styles.barTitleContainer}>
<Text h2 color={palette.onBackgroundDark01} style={styles.headerTitle}>
{title}
</Text>
<Text caption2 color={palette.onBackgroundDark01}>
{subtitle}
</Text>
</View>
<View style={styles.barButton} />
</View>
);
};

type FooterProps = {
bottomInset: number;
deleteShown: boolean;
onPressDelete: () => void;
onPressDownload: () => void;
};
const FileViewerFooter = ({ bottomInset, deleteShown, onPressDelete, onPressDownload }: FooterProps) => {
const { palette } = useUIKitTheme();
const { defaultHeight } = useHeaderStyle();
const { left, right } = useSafeAreaInsets();

return (
<View
style={[
styles.footerContainer,
{
paddingLeft: styles.headerContainer.paddingHorizontal + left,
paddingRight: styles.headerContainer.paddingHorizontal + right,
},
{
paddingBottom: bottomInset,
height: defaultHeight + bottomInset,
backgroundColor: palette.overlay01,
},
]}
>
<SBUPressable as={'TouchableOpacity'} onPress={onPressDownload} style={styles.barButton}>
<Icon icon={'download'} size={24} color={palette.onBackgroundDark01} />
</SBUPressable>
<View style={styles.barTitleContainer} />
<SBUPressable as={'TouchableOpacity'} onPress={onPressDelete} style={styles.barButton} disabled={!deleteShown}>
{deleteShown && <Icon icon={'delete'} size={24} color={palette.onBackgroundDark01} />}
</SBUPressable>
</View>
);
};

const styles = createStyleSheet({
headerContainer: {
top: 0,
left: 0,
right: 0,
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 12,
},
barButton: {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center',
},
barTitleContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
marginBottom: 2,
},
footerContainer: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 12,
},
});

export default FileViewer;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getFileType,
isMyMessage,
messageKeyExtractor,
toMegabyte,
useFreshCallback,
} from '@sendbird/uikit-utils';

Expand Down Expand Up @@ -138,7 +139,6 @@ const GroupChannelMessageList = ({
};

type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage;
const toMegabyte = (byte: number) => byte / 1024 / 1024;
const useGetMessagePressActions = ({
onPressImageMessage,
onPressMediaMessage,
Expand Down Expand Up @@ -266,7 +266,7 @@ const useGetMessagePressActions = ({
Logger.warn(DEPRECATION_WARNING.GROUP_CHANNEL.ON_PRESS_IMAGE_MESSAGE);
onPressImageMessage(msg, getAvailableUriFromFileMessage(msg));
}
onPressMediaMessage?.(msg, getAvailableUriFromFileMessage(msg), fileType);
onPressMediaMessage?.(msg, () => onDeleteMessage(msg), getAvailableUriFromFileMessage(msg));
};
break;
}
Expand Down
Loading

0 comments on commit 9b9d52b

Please sign in to comment.