From f33fe2c54b00487d420979545505cc5c6c58c51b Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Sat, 29 Jul 2023 19:14:19 +0300 Subject: [PATCH] Add share intent Signed-off-by: Nikolaos Karaolidis --- android/app/src/main/AndroidManifest.xml | 18 ++++++++ package-lock.json | 24 ++++++++++ package.json | 2 + patches/react-native-share-menu+6.0.0.patch | 38 +++++++++++++++ src/components/floatingActionButton.tsx | 14 ++---- src/navigation.tsx | 34 ++++++++++++-- src/screens/editors/addMeme.tsx | 22 +++++---- src/types/index.ts | 8 +++- src/types/route.ts | 51 ++++++++++++++++++++- src/types/share.ts | 7 +++ src/utilities/filesystem.ts | 23 ++++++++++ src/utilities/index.ts | 2 + src/utilities/permissions.ts | 7 ++- 13 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 patches/react-native-share-menu+6.0.0.patch create mode 100644 src/types/share.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e6a83b1..cb64fb0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 4ebc6e3..ebfa808 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", "react-native-share": "^9.2.3", + "react-native-share-menu": "^6.0.0", "react-native-vector-icons": "^9.2.0", "react-native-video": "^6.0.0-alpha.6", "react-redux": "^8.1.1", @@ -49,6 +50,7 @@ "@types/jest": "^29.5.2", "@types/metro-config": "^0.76.3", "@types/react": "^18.2.14", + "@types/react-native-share-menu": "^5.0.2", "@types/react-native-vector-icons": "^6.4.13", "@types/react-native-video": "^5.0.15", "@types/react-test-renderer": "^18.0.0", @@ -4615,6 +4617,12 @@ "@types/react": "*" } }, + "node_modules/@types/react-native-share-menu": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/react-native-share-menu/-/react-native-share-menu-5.0.2.tgz", + "integrity": "sha512-Qa9DGfL6Bvng2DXgCK0fFzdi9SJMGfs06MLSkCfSXBCGKlFLzSHCsXztvXlCCChn3dQArFHyz/uRUN3Sbt6LtQ==", + "dev": true + }, "node_modules/@types/react-native-vector-icons": { "version": "6.4.13", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.13.tgz", @@ -13542,6 +13550,11 @@ "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.3.tgz", "integrity": "sha512-y6ju4HS6ydJoPVoacZ/Hp3i47AfI9W4e76Jv00r01dVbr6SCCcuqk37kIbn+kYivdTxOW77UGEbhtBHHtXnhzg==" }, + "node_modules/react-native-share-menu": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz", + "integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A==" + }, "node_modules/react-native-vector-icons": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", @@ -19197,6 +19210,12 @@ "@types/react": "*" } }, + "@types/react-native-share-menu": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/react-native-share-menu/-/react-native-share-menu-5.0.2.tgz", + "integrity": "sha512-Qa9DGfL6Bvng2DXgCK0fFzdi9SJMGfs06MLSkCfSXBCGKlFLzSHCsXztvXlCCChn3dQArFHyz/uRUN3Sbt6LtQ==", + "dev": true + }, "@types/react-native-vector-icons": { "version": "6.4.13", "resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.13.tgz", @@ -25904,6 +25923,11 @@ "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.3.tgz", "integrity": "sha512-y6ju4HS6ydJoPVoacZ/Hp3i47AfI9W4e76Jv00r01dVbr6SCCcuqk37kIbn+kYivdTxOW77UGEbhtBHHtXnhzg==" }, + "react-native-share-menu": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz", + "integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A==" + }, "react-native-vector-icons": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", diff --git a/package.json b/package.json index d376534..9c8e06f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", "react-native-share": "^9.2.3", + "react-native-share-menu": "^6.0.0", "react-native-vector-icons": "^9.2.0", "react-native-video": "^6.0.0-alpha.6", "react-redux": "^8.1.1", @@ -54,6 +55,7 @@ "@types/jest": "^29.5.2", "@types/metro-config": "^0.76.3", "@types/react": "^18.2.14", + "@types/react-native-share-menu": "^5.0.2", "@types/react-native-vector-icons": "^6.4.13", "@types/react-native-video": "^5.0.15", "@types/react-test-renderer": "^18.0.0", diff --git a/patches/react-native-share-menu+6.0.0.patch b/patches/react-native-share-menu+6.0.0.patch new file mode 100644 index 0000000..89daa38 --- /dev/null +++ b/patches/react-native-share-menu+6.0.0.patch @@ -0,0 +1,38 @@ +diff --git a/node_modules/react-native-share-menu/android/build.gradle b/node_modules/react-native-share-menu/android/build.gradle +index 9557fdb..b0503cb 100644 +--- a/node_modules/react-native-share-menu/android/build.gradle ++++ b/node_modules/react-native-share-menu/android/build.gradle +@@ -1,12 +1,12 @@ + apply plugin: 'com.android.library' + + android { +- compileSdkVersion 29 +- buildToolsVersion "29.0.2" ++ compileSdkVersion 33 ++ buildToolsVersion "33.0.0" + + defaultConfig { +- minSdkVersion 16 +- targetSdkVersion 29 ++ minSdkVersion 21 ++ targetSdkVersion 33 + versionCode 1 + versionName "1.0" + ndk { +diff --git a/node_modules/react-native-share-menu/android/src/main/java/com/meedan/ShareMenuModule.java b/node_modules/react-native-share-menu/android/src/main/java/com/meedan/ShareMenuModule.java +index 09abd7b..af552b1 100644 +--- a/node_modules/react-native-share-menu/android/src/main/java/com/meedan/ShareMenuModule.java ++++ b/node_modules/react-native-share-menu/android/src/main/java/com/meedan/ShareMenuModule.java +@@ -163,4 +163,12 @@ public class ShareMenuModule extends ReactContextBaseJavaModule implements Activ + // Update intent in case the user calls `getSharedText` again + currentActivity.setIntent(intent); + } ++ ++ @ReactMethod ++ public void addListener(String eventName) { ++ } ++ ++ @ReactMethod ++ public void removeListeners(Integer count) { ++ } + } diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 9a5d3b1..432cbdd 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -5,7 +5,7 @@ import { ParamListBase, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { pick } from 'react-native-document-picker'; import { useDeviceOrientation } from '@react-native-community/hooks'; -import { ROUTE } from '../types'; +import { documentPickerResponseToAddMemeFile, ROUTE } from '../types'; import { allowedMimeTypes, noOp } from '../utilities'; const floatingActionButtonStyles = StyleSheet.create({ @@ -54,22 +54,16 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { label: 'Tag', onPress: () => navigate(ROUTE.ADD_TAG), }, - { - icon: 'note-text', - label: 'Text', - onPress: () => { - throw new Error('Not yet implemented'); - }, - }, ]} onStateChange={({ open }) => setState(open)} onPress={async () => { if (!state) return; - const files = await pick({ + const response = await pick({ type: allowedMimeTypes, allowMultiSelection: true, }).catch(noOp); - if (!files) return; + if (!response) return; + const files = documentPickerResponseToAddMemeFile(response); navigate(ROUTE.ADD_MEME, { files }); }} style={ diff --git a/src/navigation.tsx b/src/navigation.tsx index 76384cd..b2ffcc0 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -1,5 +1,9 @@ -import React from 'react'; -import { NavigationContainer as NavigationContainerBase } from '@react-navigation/native'; +import React, { useCallback, useEffect } from 'react'; +import { + NavigationContainer as NavigationContainerBase, + ParamListBase, + createNavigationContainerRef, +} from '@react-navigation/native'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; @@ -20,8 +24,14 @@ import { FloatingActionButton, HideableBottomNavigationBar, } from './components'; -import { ROUTE, RootStackParamList } from './types'; +import { + ROUTE, + RootStackParamList, + SharedItem, + sharedItemToAddMemeFile, +} from './types'; import { RootState } from './state'; +import ShareMenu from 'react-native-share-menu'; const TabNavigator = () => { const navVisible = useSelector( @@ -84,10 +94,28 @@ const TabNavigator = () => { const NavigationContainer = () => { const theme = useTheme(); + const navigationRef = createNavigationContainerRef(); + + const handleShare = useCallback( + (item: SharedItem | undefined) => { + if (!item) return; + const files = sharedItemToAddMemeFile(item); + navigationRef.current?.navigate(ROUTE.ADD_MEME, { files }); + }, + [navigationRef], + ); + + useEffect(() => { + ShareMenu.getInitialShare(handleShare); + const listener = ShareMenu.addNewShareListener(handleShare); + return () => listener.remove(); + }, [handleShare]); + const StackNavigatorBase = createNativeStackNavigator(); return ( (); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeIsFavorite, setMemeIsFavorite] = useState(false); @@ -105,7 +107,7 @@ const AddMeme = ({ file.current = files.current[index + 1]; setMemeUri(file.current.uri); - setMemeFilename(file.current.name ?? undefined); + setMemeFilename(file.current.filename); setMemeTitle(validateMemeTitle('New Meme')); setMemeIsFavorite(false); setMemeTags(new Map()); @@ -117,14 +119,16 @@ const AddMeme = ({ setIsSavingAndAddingMore(false); setIndex(0); - files.current = (await pick({ + const response = await pick({ type: allowedMimeTypes, allowMultiSelection: true, - }).catch(goBack)) as DocumentPickerResponse[]; + }).catch(goBack); + if (!response) return; + files.current = documentPickerResponseToAddMemeFile(response); file.current = files.current[0]; setMemeUri(file.current.uri); - setMemeFilename(file.current.name ?? undefined); + setMemeFilename(file.current.filename); setMemeTitle(validateMemeTitle('New Meme')); setMemeIsFavorite(false); setMemeTags(new Map()); diff --git a/src/types/index.ts b/src/types/index.ts index b256712..a7cbce0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,11 @@ export { type Dimensions } from './dimensions'; -export { ROUTE, type RootStackParamList } from './route'; +export { + ROUTE, + type RootStackParamList, + documentPickerResponseToAddMemeFile, + sharedItemToAddMemeFile, +} from './route'; +export { type SharedItem } from './share'; export { MEME_SORT, memesSortQuery, diff --git a/src/types/route.ts b/src/types/route.ts index 46541f4..fcb64e6 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -1,4 +1,6 @@ import { DocumentPickerResponse } from 'react-native-document-picker'; +import { getFilenameFromUri, guessMimeType } from '../utilities'; +import { SharedItem } from './share'; enum ROUTE { MAIN = 'Main', @@ -17,8 +19,48 @@ interface MemeViewRouteParams { index: number; } +interface AddMemeFile { + uri: string; + filename: string; + type?: string; +} + +const documentPickerResponseToAddMemeFile = ( + response: DocumentPickerResponse[], +): AddMemeFile[] => { + return response.map(item => { + const { uri, name, type } = item; + + return { + uri, + filename: name ?? getFilenameFromUri(uri), + type: type ?? guessMimeType(uri), + }; + }); +}; + +const sharedItemToAddMemeFile = (item: SharedItem): AddMemeFile[] => { + const { data, mimeType } = item; + + if (typeof data === 'string') { + return [ + { + uri: data, + filename: getFilenameFromUri(data), + type: mimeType, + }, + ]; + } + + return data.map(uri => ({ + uri, + filename: getFilenameFromUri(uri), + type: guessMimeType(uri), + })); +}; + interface AddMemeRouteParams { - files: DocumentPickerResponse[]; + files: AddMemeFile[]; } interface EditMemeRouteParams { @@ -44,4 +86,9 @@ interface RootStackParamList { [ROUTE.EDIT_TAG]: EditTagRouteParams; } -export { ROUTE, type RootStackParamList }; +export { + ROUTE, + type RootStackParamList, + documentPickerResponseToAddMemeFile, + sharedItemToAddMemeFile, +}; diff --git a/src/types/share.ts b/src/types/share.ts new file mode 100644 index 0000000..79feab4 --- /dev/null +++ b/src/types/share.ts @@ -0,0 +1,7 @@ +interface SharedItem { + data: string | string[]; + mimeType: string; + extraData?: object; +} + +export { type SharedItem }; diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts index ebc8581..0b0bf67 100644 --- a/src/utilities/filesystem.ts +++ b/src/utilities/filesystem.ts @@ -25,9 +25,32 @@ const getMemeType = (mimeType: string): MEME_TYPE | undefined => { } }; +const guessMimeType = (filename: string): string | undefined => { + const extension = filename.split('.').pop()?.toLowerCase(); + switch (extension) { + case 'bmp': { + return 'image/bmp'; + } + case 'jpg': + case 'jpeg': { + return 'image/jpeg'; + } + case 'png': { + return 'image/png'; + } + case 'webp': { + return 'image/webp'; + } + case 'gif': { + return 'image/gif'; + } + } +}; + export { allowedImageMimeTypes, allowedGifMimeTypes, allowedMimeTypes, getMemeType, + guessMimeType, }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 65f5dc6..ea34b91 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -14,6 +14,7 @@ export { allowedGifMimeTypes, allowedMimeTypes, getMemeType, + guessMimeType, } from './filesystem'; export { getSortIcon, getViewIcon } from './icon'; export { @@ -26,6 +27,7 @@ export { export { isPermissionForPath, clearPermissions, + getFilenameFromUri, } from './permissions'; export { deleteTag } from './tag'; export { diff --git a/src/utilities/permissions.ts b/src/utilities/permissions.ts index 413fdd6..1b2931b 100644 --- a/src/utilities/permissions.ts +++ b/src/utilities/permissions.ts @@ -1,3 +1,4 @@ +import { Util } from 'react-native-file-access'; import { getPersistedUriPermissions, releasePersistableUriPermission, @@ -16,4 +17,8 @@ const clearPermissions = async (excepts: string[] = []) => { }); }; -export { isPermissionForPath, clearPermissions }; +const getFilenameFromUri = (uri: string) => { + return Util.basename(uri.replaceAll('%2F', '/')); +}; + +export { isPermissionForPath, clearPermissions, getFilenameFromUri };