From 2dedd769eada69b25a972af9819d12ae9ad269e5 Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 23 Jan 2024 14:16:12 +0900 Subject: [PATCH 1/5] chore: update dependencies --- packages/uikit-chat-hooks/package.json | 2 +- packages/uikit-react-native/package.json | 1 + packages/uikit-testing-tools/package.json | 2 +- yarn.lock | 9 ++++++++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/uikit-chat-hooks/package.json b/packages/uikit-chat-hooks/package.json index d1d679aed..b78479444 100644 --- a/packages/uikit-chat-hooks/package.json +++ b/packages/uikit-chat-hooks/package.json @@ -55,7 +55,7 @@ "typescript": "5.2.2" }, "peerDependencies": { - "@sendbird/chat": "^4.9.8", + "@sendbird/chat": "^4.10.7", "react": ">=16.13.1" }, "react-native-builder-bob": { diff --git a/packages/uikit-react-native/package.json b/packages/uikit-react-native/package.json index 398cd17e8..79e35674c 100644 --- a/packages/uikit-react-native/package.json +++ b/packages/uikit-react-native/package.json @@ -59,6 +59,7 @@ "access": "public" }, "dependencies": { + "@openspacelabs/react-native-zoomable-view": "^2.1.5", "@sendbird/uikit-chat-hooks": "3.3.0", "@sendbird/uikit-react-native-foundation": "3.3.0", "@sendbird/uikit-tools": "0.0.1-alpha.57", diff --git a/packages/uikit-testing-tools/package.json b/packages/uikit-testing-tools/package.json index 2853e50a5..1a788836c 100644 --- a/packages/uikit-testing-tools/package.json +++ b/packages/uikit-testing-tools/package.json @@ -39,7 +39,7 @@ "access": "public" }, "devDependencies": { - "@sendbird/chat": "^4.9.8", + "@sendbird/chat": "^4.10.7", "@sendbird/uikit-utils": "3.3.0", "@types/jest": "^29.4.0", "@types/react": "*", diff --git a/yarn.lock b/yarn.lock index 6f940fddb..81e163e2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,6 +3077,13 @@ dependencies: "@octokit/openapi-types" "^16.0.0" +"@openspacelabs/react-native-zoomable-view@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@openspacelabs/react-native-zoomable-view/-/react-native-zoomable-view-2.1.5.tgz#e738a6493733461f0ab8327033b3dfbee2e93998" + integrity sha512-AE2y7S+8cFXXrrpKJQVrTCLh5E9Iu506/T1xgDBx9SJeiUeNFCIyoZdWhAAtsmppY5vLaUeI7qz+JVBDzmQsEA== + dependencies: + prop-types "^15.7.2" + "@parcel/watcher@2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" @@ -3372,7 +3379,7 @@ dependencies: nanoid "^3.1.23" -"@sendbird/chat@4.10.7", "@sendbird/chat@^4.10.7", "@sendbird/chat@^4.9.8": +"@sendbird/chat@4.10.7", "@sendbird/chat@^4.10.7": version "4.10.7" resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.10.7.tgz#45b23849a854273811c36a072044e64c6a0bc4bd" integrity sha512-68h9sj5mMaBzYMfcX0G7TU91JMrGRz0rbUJj6pSLet911pRMBpUi0f6smS8XtUJMNdXtwFh56TyMyAZQM6TcCQ== From 06a4f95b8d7d7b6f574f2d3a4ea15aebbad6e55b Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 23 Jan 2024 17:53:57 +0900 Subject: [PATCH 2/5] feat: implement zoomable image viewer to FileViewer --- .../src/components/FileViewer.tsx | 288 ------------------ .../FileViewer/FileViewerContent.tsx | 132 ++++++++ .../FileViewer/FileViewerFooter.tsx | 73 +++++ .../FileViewer/FileViewerHeader.tsx | 86 ++++++ .../src/components/FileViewer/index.tsx | 139 +++++++++ 5 files changed, 430 insertions(+), 288 deletions(-) delete mode 100644 packages/uikit-react-native/src/components/FileViewer.tsx create mode 100644 packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx create mode 100644 packages/uikit-react-native/src/components/FileViewer/FileViewerFooter.tsx create mode 100644 packages/uikit-react-native/src/components/FileViewer/FileViewerHeader.tsx create mode 100644 packages/uikit-react-native/src/components/FileViewer/index.tsx diff --git a/packages/uikit-react-native/src/components/FileViewer.tsx b/packages/uikit-react-native/src/components/FileViewer.tsx deleted file mode 100644 index 70486b02e..000000000 --- a/packages/uikit-react-native/src/components/FileViewer.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { StatusBar, StyleSheet, TouchableOpacity, 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, - truncate, - useIIFE, -} from '@sendbird/uikit-utils'; - -import { useLocalization, usePlatformService, useSendbirdChat } from '../hooks/useContext'; - -type Props = { - fileMessage: SendbirdFileMessage; - deleteMessage: () => Promise; - - 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 (fileType === 'file') onClose(); - }, []); - - const fileViewer = useIIFE(() => { - switch (fileType) { - case 'image': { - return ( - setLoading(false)} - /> - ); - } - - case 'video': - case 'audio': { - return ( - setLoading(false)} - /> - ); - } - - default: { - return null; - } - } - }); - - const _onPressDelete = () => { - if (!canDelete) return; - - if (onPressDelete) { - onPressDelete(fileMessage); - } else { - alert({ - title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE, - buttons: [ - { - text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL, - }, - { - text: STRINGS.LABELS.CHANNEL_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 ( - - - - {fileViewer} - {loading && } - - {headerShown && ( - - )} - - - ); -}; - -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 ( - - - - - - - {truncate(title, { mode: 'mid', maxLen: 18 })} - - - {subtitle} - - - - - ); -}; - -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 ( - - - - - - - {deleteShown && } - - - ); -}; - -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; diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx new file mode 100644 index 000000000..f8f020d9a --- /dev/null +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx @@ -0,0 +1,132 @@ +import { ReactNativeZoomableView, ReactNativeZoomableViewProps } from '@openspacelabs/react-native-zoomable-view'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import { ImageProps, ImageStyle, ImageURISource, StyleProp, StyleSheet, useWindowDimensions } from 'react-native'; + +import { SBUUtils } from '@sendbird/uikit-react-native'; +import { + Box, + Image, + LoadingSpinner, + createStyleSheet, + useHeaderStyle, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; +import { FileType, useIIFE } from '@sendbird/uikit-utils'; + +import { usePlatformService } from '../../hooks/useContext'; + +type Props = { + type: FileType; + src: string; + topInset?: number; + bottomInset?: number; + maxZoom?: number; + minZoom?: number; + onPress?: () => void; +}; +const FileViewerContent = ({ type, src, topInset = 0, bottomInset = 0, maxZoom = 4, minZoom = 1, onPress }: Props) => { + const [loading, setLoading] = useState(true); + + const { defaultHeight } = useHeaderStyle(); + const { mediaService } = usePlatformService(); + const { palette } = useUIKitTheme(); + + const source = { uri: src }; + const onLoadEnd = () => setLoading(false); + const mediaViewer = useIIFE(() => { + switch (type) { + case 'image': { + return ( + + ); + } + + case 'video': + case 'audio': { + return ( + + ); + } + + default: { + return null; + } + } + }); + + return ( + + {mediaViewer} + {loading && } + + ); +}; + +const ZoomableImageView = ({ + zoomProps, + ...props +}: { + source: ImageURISource; + style: StyleProp; + resizeMode: ImageProps['resizeMode']; + onLoadEnd: () => void; + zoomProps?: ReactNativeZoomableViewProps; +}) => { + const { width, height } = useWindowDimensions(); + + const imageSize = useRef<{ width: number; height: number }>(); + const [contentSizeProps, setContentSizeProps] = useState({}); + + useLayoutEffect(() => { + SBUUtils.safeRun(async () => { + if (props.source.uri) { + const image = imageSize.current ?? (await SBUUtils.getImageSize(props.source.uri)); + imageSize.current = image; + + const viewRatio = width / height; + const imageRatio = image.width / image.height; + + const fitDirection = viewRatio > imageRatio ? 'height' : 'width'; + const ratio = fitDirection === 'height' ? height / image.height : width / image.width; + const actualSize = { width: image.width * ratio, height: image.height * ratio }; + + setContentSizeProps({ + contentWidth: actualSize.width, + contentHeight: actualSize.height, + }); + } + }); + }, [props.source.uri, width, height]); + + return ( + + + + ); +}; + +const styles = createStyleSheet({ + container: { + zIndex: -1, + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default FileViewerContent; diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerFooter.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerFooter.tsx new file mode 100644 index 000000000..5f34b481e --- /dev/null +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerFooter.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { + Box, + Icon, + PressBox, + createStyleSheet, + useHeaderStyle, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; + +type Props = { + bottomInset: number; + deleteShown: boolean; + onPressDelete: () => void; + onPressDownload: () => void; +}; + +const FileViewerFooter = ({ bottomInset, deleteShown, onPressDelete, onPressDownload }: Props) => { + const { palette } = useUIKitTheme(); + const { defaultHeight } = useHeaderStyle(); + const { left, right } = useSafeAreaInsets(); + + return ( + + + + + + + {deleteShown && } + + + ); +}; + +const styles = createStyleSheet({ + container: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 12, + }, + buttonContainer: { + width: 32, + height: 32, + alignItems: 'center', + justifyContent: 'center', + }, + titleContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default FileViewerFooter; diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerHeader.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerHeader.tsx new file mode 100644 index 000000000..13c5f1c72 --- /dev/null +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerHeader.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { + Box, + Icon, + PressBox, + Text, + createStyleSheet, + useHeaderStyle, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; +import { truncate } from '@sendbird/uikit-utils'; + +type Props = { + headerShown?: boolean; + topInset: number; + onClose: () => void; + title: string; + subtitle: string; +}; + +const FileViewerHeader = ({ headerShown = true, topInset, onClose, subtitle, title }: Props) => { + const { palette } = useUIKitTheme(); + const { defaultHeight } = useHeaderStyle(); + const { left, right } = useSafeAreaInsets(); + + if (!headerShown) return null; + + return ( + + + + + + + {truncate(title, { mode: 'mid', maxLen: 18 })} + + + {subtitle} + + + + + ); +}; + +const styles = createStyleSheet({ + container: { + top: 0, + left: 0, + right: 0, + position: 'absolute', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 12, + }, + buttonContainer: { + width: 32, + height: 32, + alignItems: 'center', + justifyContent: 'center', + }, + titleContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + title: { + marginBottom: 2, + }, +}); + +export default FileViewerHeader; diff --git a/packages/uikit-react-native/src/components/FileViewer/index.tsx b/packages/uikit-react-native/src/components/FileViewer/index.tsx new file mode 100644 index 000000000..acc544177 --- /dev/null +++ b/packages/uikit-react-native/src/components/FileViewer/index.tsx @@ -0,0 +1,139 @@ +import React, { useEffect } from 'react'; +import { StatusBar } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { Box, useAlert, useHeaderStyle, useToast, useUIKitTheme } from '@sendbird/uikit-react-native-foundation'; +import type { SendbirdFileMessage } from '@sendbird/uikit-utils'; +import { Logger, getFileExtension, getFileType, isMyMessage, toMegabyte } from '@sendbird/uikit-utils'; + +import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext'; +import FileViewerContent from './FileViewerContent'; +import FileViewerFooter from './FileViewerFooter'; +import FileViewerHeader from './FileViewerHeader'; + +type Props = { + fileMessage: SendbirdFileMessage; + deleteMessage: () => Promise; + + onClose: () => void; + onPressDownload?: (message: SendbirdFileMessage) => void; + onPressDelete?: (message: SendbirdFileMessage) => void; + + headerShown?: boolean; + headerTopInset?: number; + + /** This prop is only available on the Image viewer */ + minZoom?: number; + /** This prop is only available on the Image viewer */ + maxZoom?: number; +}; + +const FileViewer = ({ + headerShown = true, + maxZoom = 3, + minZoom = 1, + headerTopInset, + fileMessage, + onClose, + onPressDownload, + onPressDelete, + deleteMessage, +}: Props) => { + const { topInset, statusBarTranslucent } = useHeaderStyle(); + const { bottom } = useSafeAreaInsets(); + const { palette } = useUIKitTheme(); + const { alert } = useAlert(); + const { show } = useToast(); + + const { fileService } = usePlatformService(); + const { currentUser } = useSendbirdChat(); + const { STRINGS } = useLocalization(); + + const fileType = getFileType(fileMessage.type || getFileExtension(fileMessage.url)); + const canDelete = isMyMessage(fileMessage, currentUser?.userId); + const basicTopInset = statusBarTranslucent ? topInset : 0; + + const onPressDeleteButton = () => { + if (!canDelete) return; + + if (onPressDelete) { + onPressDelete(fileMessage); + } else { + alert({ + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE, + buttons: [ + { + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL, + }, + { + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK, + style: 'destructive', + onPress: () => { + deleteMessage() + .then(() => { + onClose(); + }) + .catch(() => { + show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error'); + }); + }, + }, + ], + }); + } + }; + + const onPressDownloadButton = () => { + if (onPressDownload) { + onPressDownload(fileMessage); + } else { + if (toMegabyte(fileMessage.size) > 4) { + show(STRINGS.TOAST.DOWNLOAD_START, 'success'); + } + + fileService + .save({ fileUrl: fileMessage.url, fileName: fileMessage.name, fileType: fileMessage.type }) + .then((response) => { + show(STRINGS.TOAST.DOWNLOAD_OK, 'success'); + Logger.log('File saved to', response); + }) + .catch((err) => { + show(STRINGS.TOAST.DOWNLOAD_ERROR, 'error'); + Logger.log('File save failure', err); + }); + } + }; + + useEffect(() => { + if (fileType === 'file') onClose(); + }, []); + + return ( + + + + + + + ); +}; + +export default FileViewer; From 38bc9e55fc4779b2f361323663f506fb15a40459 Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 23 Jan 2024 17:58:46 +0900 Subject: [PATCH 3/5] chore: resolve build error --- .../src/components/FileViewer/FileViewerContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx index f8f020d9a..33f5631bd 100644 --- a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx @@ -2,7 +2,6 @@ import { ReactNativeZoomableView, ReactNativeZoomableViewProps } from '@openspac import React, { useLayoutEffect, useRef, useState } from 'react'; import { ImageProps, ImageStyle, ImageURISource, StyleProp, StyleSheet, useWindowDimensions } from 'react-native'; -import { SBUUtils } from '@sendbird/uikit-react-native'; import { Box, Image, @@ -14,6 +13,7 @@ import { import { FileType, useIIFE } from '@sendbird/uikit-utils'; import { usePlatformService } from '../../hooks/useContext'; +import SBUUtils from '../../libs/SBUUtils'; type Props = { type: FileType; From 2a9f40a485429825bf08d2f46abeda82cc6f604e Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 23 Jan 2024 19:28:18 +0900 Subject: [PATCH 4/5] chore: add initial value to contentSizeProps --- .../src/components/FileViewer/FileViewerContent.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx index 33f5631bd..d1fa73a5a 100644 --- a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx @@ -90,7 +90,10 @@ const ZoomableImageView = ({ const { width, height } = useWindowDimensions(); const imageSize = useRef<{ width: number; height: number }>(); - const [contentSizeProps, setContentSizeProps] = useState({}); + const [contentSizeProps, setContentSizeProps] = useState({ + contentWidth: width, + contentHeight: height, + }); useLayoutEffect(() => { SBUUtils.safeRun(async () => { From 140b8e8b4519000564a716ee0d964bc828171f93 Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 23 Jan 2024 19:29:31 +0900 Subject: [PATCH 5/5] chore: disable visualTouchFeedbackEnabled --- .../src/components/FileViewer/FileViewerContent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx index d1fa73a5a..d8be62de4 100644 --- a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx @@ -117,7 +117,13 @@ const ZoomableImageView = ({ }, [props.source.uri, width, height]); return ( - + );