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 };