From e479e3c0ad97570ab300a33169f52124b4edb84e Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Mon, 24 Jul 2023 21:55:36 +0300 Subject: [PATCH] Add meme view & sharing Signed-off-by: Nikolaos Karaolidis --- .gitlab-ci.yml | 17 +- index.js | 3 - package-lock.json | 54 ++---- package.json | 2 +- src/components/index.ts | 5 +- .../memes/gridView/memesGridView.tsx | 74 -------- src/components/memes/index.ts | 5 +- .../memes/listView/memesListView.tsx | 68 ------- .../memes/masonryView/memesMasonryView.tsx | 75 -------- src/components/memes/memeEditor.tsx | 34 ++-- src/components/memes/memeViewItem.tsx | 53 ++++++ .../{gridView => memesList}/memesGridItem.tsx | 18 +- src/components/memes/memesList/memesList.tsx | 167 ++++++++++++++++++ .../{listView => memesList}/memesListItem.tsx | 17 +- .../memesMasonryItem.tsx | 17 +- src/components/tags/tagEditor.tsx | 19 +- src/database/meme.ts | 2 + src/navigation.tsx | 7 +- src/screens/addMeme.tsx | 79 +++++++-- src/screens/addTag.tsx | 51 +++++- src/screens/editMeme.tsx | 57 +++--- src/screens/editTag.tsx | 37 ++-- src/screens/index.ts | 1 + src/screens/memeView.tsx | 136 ++++++++++++++ src/screens/memes.tsx | 87 +++------ src/screens/tags.tsx | 6 +- src/styles.tsx | 4 + src/{theme.tsx => theme.ts} | 9 +- src/types/route.ts | 14 +- src/utilities/color.ts | 10 ++ src/utilities/index.ts | 11 +- src/utilities/meme.ts | 51 ++++++ src/utilities/tag.ts | 16 ++ 33 files changed, 724 insertions(+), 482 deletions(-) delete mode 100644 src/components/memes/gridView/memesGridView.tsx delete mode 100644 src/components/memes/listView/memesListView.tsx delete mode 100644 src/components/memes/masonryView/memesMasonryView.tsx create mode 100644 src/components/memes/memeViewItem.tsx rename src/components/memes/{gridView => memesList}/memesGridItem.tsx (71%) create mode 100644 src/components/memes/memesList/memesList.tsx rename src/components/memes/{listView => memesList}/memesListItem.tsx (87%) rename src/components/memes/{masonryView => memesList}/memesMasonryItem.tsx (78%) create mode 100644 src/screens/memeView.tsx rename src/{theme.tsx => theme.ts} (95%) create mode 100644 src/utilities/meme.ts create mode 100644 src/utilities/tag.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d25cdaa..8a42d9e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,10 +12,8 @@ build: - package-lock.json paths: - node_modules - policy: pull before_script: - npm install - - npm run postinstall script: - npm run build rules: @@ -30,10 +28,8 @@ compile: - package-lock.json paths: - node_modules - policy: pull before_script: - npm install - - npm run postinstall script: - npm run compile after_script: @@ -56,10 +52,8 @@ test: - package-lock.json paths: - node_modules - policy: pull before_script: - npm install - - npm run postinstall script: - npm run test rules: @@ -77,7 +71,6 @@ lint: policy: pull before_script: - npm install - - npm run postinstall script: - npm run lint rules: @@ -90,12 +83,12 @@ release: - job: compile artifacts: true script: - - echo "Create Release $CI_COMMIT_SHA" + - echo "Create Release $CI_COMMIT_TAG" release: - name: "Release $CI_COMMIT_SHORT_SHA" - tag_name: "$CI_COMMIT_SHORT_SHA" - ref: "$CI_COMMIT_SHORT_SHA" - description: "Release $CI_COMMIT_SHORT_SHA" + name: "Release $CI_COMMIT_TAG" + tag_name: "$CI_COMMIT_TAG" + ref: "$CI_COMMIT_TAG" + description: "Release $CI_COMMIT_SHA" assets: links: - name: "app-release.apk" diff --git a/index.js b/index.js index df06f51..28ad900 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,6 @@ import { AppRegistry } from 'react-native'; import 'react-native-get-random-values'; -import { enableFreeze } from 'react-native-screens'; import App from './src/app'; import { name as appName } from './app.json'; -enableFreeze(true); - AppRegistry.registerComponent(appName, () => App); diff --git a/package-lock.json b/package-lock.json index 65d5f12..551a997 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@bankify/redux-persist-realm": "^0.1.3", + "@likashefqet/react-native-image-zoom": "^1.3.0", "@react-native-clipboard/clipboard": "^1.11.2", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", @@ -23,7 +24,6 @@ "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", "react-native-get-random-values": "^1.9.0", - "react-native-image-zoom-viewer": "^3.0.1", "react-native-mime-types": "^2.4.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", @@ -2988,6 +2988,17 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, + "node_modules/@likashefqet/react-native-image-zoom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@likashefqet/react-native-image-zoom/-/react-native-image-zoom-1.3.0.tgz", + "integrity": "sha512-PLRd1hNMHe9LUn8b4rmLt86282geuaqP4Qd2rFWIloxMS2ePNTIaNlEUu3T3LaO8Pg9vhVV97TxfFeU8F+tcYQ==", + "peerDependencies": { + "react": ">=16.x.x", + "react-native": ">=0.62.x", + "react-native-gesture-handler": ">=2.x.x", + "react-native-reanimated": ">=2.x.x" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -13414,27 +13425,6 @@ "react-native": ">=0.56" } }, - "node_modules/react-native-image-pan-zoom": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz", - "integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, - "node_modules/react-native-image-zoom-viewer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz", - "integrity": "sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==", - "dependencies": { - "react-native-image-pan-zoom": "^2.1.12" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-mime-types": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz", @@ -17927,6 +17917,12 @@ } } }, + "@likashefqet/react-native-image-zoom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@likashefqet/react-native-image-zoom/-/react-native-image-zoom-1.3.0.tgz", + "integrity": "sha512-PLRd1hNMHe9LUn8b4rmLt86282geuaqP4Qd2rFWIloxMS2ePNTIaNlEUu3T3LaO8Pg9vhVV97TxfFeU8F+tcYQ==", + "requires": {} + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -25811,20 +25807,6 @@ "fast-base64-decode": "^1.0.0" } }, - "react-native-image-pan-zoom": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz", - "integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==", - "requires": {} - }, - "react-native-image-zoom-viewer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz", - "integrity": "sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==", - "requires": { - "react-native-image-pan-zoom": "^2.1.12" - } - }, "react-native-mime-types": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz", diff --git a/package.json b/package.json index 8d9d395..a9f5bb6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@bankify/redux-persist-realm": "^0.1.3", + "@likashefqet/react-native-image-zoom": "^1.3.0", "@react-native-clipboard/clipboard": "^1.11.2", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", @@ -28,7 +29,6 @@ "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", "react-native-get-random-values": "^1.9.0", - "react-native-image-zoom-viewer": "^3.0.1", "react-native-mime-types": "^2.4.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", diff --git a/src/components/index.ts b/src/components/index.ts index 554bc2b..ad5eaa2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,11 +1,10 @@ export { - MemesGridView, - MemesListView, - MemesMasonryView, + MemesList, MemeEditor, MemesHeader, MemeTagSearchModal, MemeTagSelector, + MemeViewItem, } from './memes'; export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags'; export { default as FloatingActionButton } from './floatingActionButton'; diff --git a/src/components/memes/gridView/memesGridView.tsx b/src/components/memes/gridView/memesGridView.tsx deleted file mode 100644 index 2523585..0000000 --- a/src/components/memes/gridView/memesGridView.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { RefObject } from 'react'; -import { Meme } from '../../../database'; -import { FlashList } from '@shopify/flash-list'; -import { HelperText } from 'react-native-paper'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, -} from 'react-native'; -import { useSelector } from 'react-redux'; -import styles from '../../../styles'; -import { RootState } from '../../../state'; -import { ORIENTATION, useDimensions } from '../../../contexts'; -import { getFlashListItemHeight } from '../../../utilities'; -import MemesGridItem from './memesGridItem'; - -const gridViewStyles = StyleSheet.create({ - helperText: { - marginVertical: 10, - }, - flashList: { - paddingBottom: 100, - paddingHorizontal: 2.5, - }, -}); - -const MemesGridView = ({ - memes, - flashListRef, - flashListPadding, - handleScroll, -}: { - memes: Realm.Results>; - flashListRef: RefObject>; - flashListPadding: number; - handleScroll: (event: NativeSyntheticEvent) => void; -}) => { - const { orientation, dimensions } = useDimensions(); - const gridColumns = useSelector( - (state: RootState) => state.settings.gridColumns, - ); - - return ( - } - contentContainerStyle={{ - paddingTop: - flashListPadding + - dimensions.height * - (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), - ...gridViewStyles.flashList, - }} - ListEmptyComponent={() => ( - - No memes found - - )} - onScroll={handleScroll} - /> - ); -}; - -export default MemesGridView; diff --git a/src/components/memes/index.ts b/src/components/memes/index.ts index 2095c14..116868f 100644 --- a/src/components/memes/index.ts +++ b/src/components/memes/index.ts @@ -1,7 +1,6 @@ -export { default as MemesGridView } from './gridView/memesGridView'; -export { default as MemesListView } from './listView/memesListView'; -export { default as MemesMasonryView } from './masonryView/memesMasonryView'; +export { default as MemesList } from './memesList/memesList'; export { default as MemeEditor } from './memeEditor'; export { default as MemesHeader } from './memesHeader'; export { default as MemeTagSearchModal } from './memeTagSearchModal'; export { default as MemeTagSelector } from './memeTagSelector'; +export { default as MemeViewItem } from './memeViewItem'; diff --git a/src/components/memes/listView/memesListView.tsx b/src/components/memes/listView/memesListView.tsx deleted file mode 100644 index 3afcee1..0000000 --- a/src/components/memes/listView/memesListView.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { RefObject } from 'react'; -import { Meme } from '../../../database'; -import { FlashList } from '@shopify/flash-list'; -import { Divider, HelperText } from 'react-native-paper'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, -} from 'react-native'; -import styles from '../../../styles'; -import { ORIENTATION, useDimensions } from '../../../contexts'; -import MemesListItem from './memesListItem'; - -const gridViewStyles = StyleSheet.create({ - helperText: { - marginVertical: 10, - }, - flashList: { - paddingBottom: 100, - paddingHorizontal: 5, - }, -}); - -const MemesListView = ({ - memes, - flashListRef, - flashListPadding, - handleScroll, -}: { - memes: Realm.Results>; - flashListRef: RefObject>; - flashListPadding: number; - handleScroll: (event: NativeSyntheticEvent) => void; -}) => { - const { orientation, dimensions } = useDimensions(); - - return ( - } - ItemSeparatorComponent={() => } - contentContainerStyle={{ - paddingTop: - flashListPadding + - dimensions.height * - (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), - ...gridViewStyles.flashList, - }} - ListEmptyComponent={() => ( - - No memes found - - )} - onScroll={handleScroll} - /> - ); -}; - -export default MemesListView; diff --git a/src/components/memes/masonryView/memesMasonryView.tsx b/src/components/memes/masonryView/memesMasonryView.tsx deleted file mode 100644 index 8e1e22a..0000000 --- a/src/components/memes/masonryView/memesMasonryView.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { RefObject } from 'react'; -import { Meme } from '../../../database'; -import { FlashList, MasonryFlashList } from '@shopify/flash-list'; -import { HelperText } from 'react-native-paper'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, -} from 'react-native'; -import { useSelector } from 'react-redux'; -import styles from '../../../styles'; -import { RootState } from '../../../state'; -import { ORIENTATION, useDimensions } from '../../../contexts'; -import { getFlashListItemHeight } from '../../../utilities'; -import MemesMasonryItem from './memesMasonryItem'; - -const memeMasonryViewStyles = StyleSheet.create({ - helperText: { - marginVertical: 10, - }, - flashList: { - paddingBottom: 100, - // Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876 - paddingHorizontal: 0.1, - }, -}); - -const MemesMasonryView = ({ - memes, - flashListRef, - flashListPadding, - handleScroll, -}: { - memes: Realm.Results>; - flashListRef: RefObject>; - flashListPadding: number; - handleScroll: (event: NativeSyntheticEvent) => void; -}) => { - const { orientation, dimensions } = useDimensions(); - const masonryColumns = useSelector( - (state: RootState) => state.settings.masonryColumns, - ); - - return ( - } - contentContainerStyle={{ - paddingTop: - flashListPadding + - dimensions.height * - (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), - ...memeMasonryViewStyles.flashList, - }} - ListEmptyComponent={() => ( - - No memes found - - )} - onScroll={handleScroll} - /> - ); -}; - -export default MemesMasonryView; diff --git a/src/components/memes/memeEditor.tsx b/src/components/memes/memeEditor.tsx index 0ebc4c8..18812d3 100644 --- a/src/components/memes/memeEditor.tsx +++ b/src/components/memes/memeEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { HelperText, TextInput } from 'react-native-paper'; import { Image } from 'react-native'; import { useDimensions } from '../../contexts'; @@ -21,13 +21,13 @@ const memeEditorStyles = { }; const MemeEditor = ({ - imageUri, + memeUri, memeTitle, setMemeTitle, memeTags, setMemeTags, }: { - imageUri: string; + memeUri: string; memeTitle: StringValidationResult; setMemeTitle: (name: StringValidationResult) => void; memeTags: Map; @@ -38,15 +38,23 @@ const MemeEditor = ({ const [imageWidth, setImageWidth] = useState(); const [imageHeight, setImageHeight] = useState(); - Image.getSize(imageUri, (width, height) => { - const paddedWidth = dimensions.width * 0.92; - const paddedHeight = Math.max( - Math.min((paddedWidth / width) * height, 500), - 100, - ); - setImageWidth(paddedWidth); - setImageHeight(paddedHeight); - }); + useEffect(() => { + // eslint-disable-next-line unicorn/no-useless-undefined + setImageWidth(undefined); + // eslint-disable-next-line unicorn/no-useless-undefined + setImageHeight(undefined); + + Image.getSize(memeUri, (width, height) => { + const paddedWidth = dimensions.width * 0.92; + const paddedHeight = Math.max( + Math.min((paddedWidth / width) * height, 500), + 100, + ); + + setImageWidth(paddedWidth); + setImageHeight(paddedHeight); + }); + }, [memeUri, dimensions.width]); if (!imageWidth || !imageHeight) return ; @@ -64,7 +72,7 @@ const MemeEditor = ({ {memeTitle.error} { + const { dimensions } = useDimensions(); + const { colors } = useTheme(); + + const [imageWidth, setImageWidth] = useState(); + const [imageHeight, setImageHeight] = useState(); + + useEffect(() => { + Image.getSize(meme.uri, (width, height) => { + const ratio = width / height; + const screenRatio = dimensions.width / dimensions.height - 160; + + if (ratio > screenRatio) { + setImageWidth(dimensions.width); + setImageHeight(dimensions.width / ratio); + } else { + setImageWidth(dimensions.height * ratio); + setImageHeight(dimensions.height); + } + }); + }, [meme.uri, dimensions.width, dimensions.height]); + + return ( + + {imageWidth && imageHeight ? ( + + ) : ( + + )} + + ); +}; + +export default MemeViewItem; diff --git a/src/components/memes/gridView/memesGridItem.tsx b/src/components/memes/memesList/memesGridItem.tsx similarity index 71% rename from src/components/memes/gridView/memesGridItem.tsx rename to src/components/memes/memesList/memesGridItem.tsx index e9f9e90..d211e13 100644 --- a/src/components/memes/gridView/memesGridItem.tsx +++ b/src/components/memes/memesList/memesGridItem.tsx @@ -1,14 +1,19 @@ import React, { useState } from 'react'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; import { Image, TouchableHighlight, View } from 'react-native'; import { useSelector } from 'react-redux'; import { Meme } from '../../../database'; -import { ROUTE, RootStackParamList } from '../../../types'; import { useDimensions } from '../../../contexts'; import { RootState } from '../../../state'; -const MemesGridItem = ({ meme }: { meme: Meme }) => { - const { navigate } = useNavigation>(); +const MemesGridItem = ({ + meme, + index, + focusMeme, +}: { + meme: Meme; + index: number; + focusMeme: (index: number) => void; +}) => { const { dimensions } = useDimensions(); const gridColumns = useSelector( (state: RootState) => state.settings.gridColumns, @@ -27,10 +32,7 @@ const MemesGridItem = ({ meme }: { meme: Meme }) => { <> {imageWidth && imageHeight && ( - - navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }) - }> + focusMeme(index)}> >; + flashListRef: RefObject>; + flashListPadding: number; + handleScroll: (event: NativeSyntheticEvent) => void; + focusMeme: (index: number) => void; +}) => { + const { dimensions, orientation } = useDimensions(); + const view = useSelector((state: RootState) => state.memes.view); + const masonryColumns = useSelector( + (state: RootState) => state.settings.masonryColumns, + ); + const gridColumns = useSelector( + (state: RootState) => state.settings.gridColumns, + ); + + const extraFlashListPadding = + flashListPadding + + dimensions.height * (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04); + + return ( + <> + {view === VIEW.MASONRY && ( + ( + + )} + contentContainerStyle={{ + paddingTop: extraFlashListPadding, + ...memesMasonryListStyles.flashList, + }} + ListEmptyComponent={() => ( + + No memes found + + )} + onScroll={handleScroll} + /> + )} + {view === VIEW.GRID && ( + ( + + )} + contentContainerStyle={{ + paddingTop: extraFlashListPadding + 2.5, + ...memesGridListStyles.flashList, + }} + ListEmptyComponent={() => ( + + No memes found + + )} + onScroll={handleScroll} + /> + )} + {view === VIEW.LIST && ( + ( + + )} + ItemSeparatorComponent={() => } + contentContainerStyle={{ + paddingTop: extraFlashListPadding, + ...memesListListStyles.flashList, + }} + ListEmptyComponent={() => ( + + No memes found + + )} + onScroll={handleScroll} + /> + )} + + ); +}; + +export default MemesList; diff --git a/src/components/memes/listView/memesListItem.tsx b/src/components/memes/memesList/memesListItem.tsx similarity index 87% rename from src/components/memes/listView/memesListItem.tsx rename to src/components/memes/memesList/memesListItem.tsx index 7bb419a..9f70d4c 100644 --- a/src/components/memes/listView/memesListItem.tsx +++ b/src/components/memes/memesList/memesListItem.tsx @@ -1,9 +1,7 @@ import React, { useState } from 'react'; import { Image, StyleSheet, View } from 'react-native'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; import { Text, TouchableRipple } from 'react-native-paper'; import { Meme } from '../../../database'; -import { ROUTE, RootStackParamList } from '../../../types'; import styles from '../../../styles'; import { useDimensions } from '../../../contexts'; @@ -23,8 +21,15 @@ const memesListItemStyles = StyleSheet.create({ }, }); -const MemesListItem = ({ meme }: { meme: Meme }) => { - const { navigate } = useNavigation>(); +const MemesListItem = ({ + meme, + index, + focusMeme, +}: { + meme: Meme; + index: number; + focusMeme: (index: number) => void; +}) => { const { dimensions } = useDimensions(); const [imageWidth, setImageWidth] = useState(); @@ -40,9 +45,7 @@ const MemesListItem = ({ meme }: { meme: Meme }) => { <> {imageWidth && imageHeight && ( - navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }) - } + onPress={() => focusMeme(index)} style={[memesListItemStyles.view, styles.flexRow]}> <> diff --git a/src/components/memes/masonryView/memesMasonryItem.tsx b/src/components/memes/memesList/memesMasonryItem.tsx similarity index 78% rename from src/components/memes/masonryView/memesMasonryItem.tsx rename to src/components/memes/memesList/memesMasonryItem.tsx index 6f687ae..14657b0 100644 --- a/src/components/memes/masonryView/memesMasonryItem.tsx +++ b/src/components/memes/memesList/memesMasonryItem.tsx @@ -1,9 +1,7 @@ import React, { useState } from 'react'; import { Image, StyleSheet, TouchableHighlight } from 'react-native'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; import { useSelector } from 'react-redux'; import { Meme } from '../../../database'; -import { ROUTE, RootStackParamList } from '../../../types'; import { useDimensions } from '../../../contexts'; import { RootState } from '../../../state'; @@ -17,8 +15,15 @@ const memeMasonryItemStyles = StyleSheet.create({ }, }); -const MemesMasonryItem = ({ meme }: { meme: Meme }) => { - const { navigate } = useNavigation>(); +const MemesMasonryItem = ({ + meme, + index, + focusMeme, +}: { + meme: Meme; + index: number; + focusMeme: (index: number) => void; +}) => { const { dimensions } = useDimensions(); const masonryColumns = useSelector( (state: RootState) => state.settings.masonryColumns, @@ -37,9 +42,7 @@ const MemesMasonryItem = ({ meme }: { meme: Meme }) => { <> {imageWidth && imageHeight && ( - navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }) - } + onPress={() => focusMeme(index)} style={memeMasonryItemStyles.view}> { const lastValidTagColor = useRef(tagColor.parsed); - const handleTagColorChange = (color: string) => { - const result = validateColor(color); - setTagColor(result); - if (result.valid) lastValidTagColor.current = result.parsed; - }; + useEffect(() => { + if (tagColor.valid) lastValidTagColor.current = tagColor.parsed; + }, [tagColor]); return ( <> - + setTagColor(validateColor(color))} error={!tagColor.valid} autoCorrect={false} right={ handleTagColorChange(generateRandomColor())} + onPress={() => setTagColor(validateColor(generateRandomColor()))} /> } /> diff --git a/src/database/meme.ts b/src/database/meme.ts index 7e3c030..67df4e6 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -22,6 +22,7 @@ class Meme extends Object { id!: BSON.UUID; type!: MEME_TYPE; uri!: string; + mimeType!: string; size!: number; title!: string; isFavorite!: boolean; @@ -39,6 +40,7 @@ class Meme extends Object { id: { type: 'uuid', default: () => new BSON.UUID() }, type: { type: 'string', indexed: true }, uri: 'string', + mimeType: 'string', size: 'int', title: 'string', isFavorite: { type: 'bool', indexed: true, default: false }, diff --git a/src/navigation.tsx b/src/navigation.tsx index 680f9f3..76384cd 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -13,6 +13,7 @@ import { EditTag, AddMeme, AddTag, + MemeView, } from './screens'; import { darkNavigationTheme, lightNavigationTheme } from './theme'; import { @@ -91,10 +92,14 @@ const NavigationContainer = () => { + state.settings.storageUri, )!; - const { file } = route.params; + const file = useRef(route.params.file); + const [memeUri, setMemeUri] = useState(file.current.uri); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeIsFavorite, setMemeIsFavorite] = useState(false); const [memeTags, setMemeTags] = useState(new Map()); const [isSaving, setIsSaving] = useState(false); + const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] = + useState(false); - const handleSave = async () => { - setIsSaving(true); - + const handleSave = useCallback(async () => { const uuid = new BSON.UUID(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mimeType = file.type!; + const mimeType = file.current.type!; const memeType = getMemeType(mimeType); - const fileExtension = extension(mimeType); + const fileExtension = extension(mimeType) as string; if (!fileExtension) goBack(); const uri = AndroidScoped.appendPath( storageUri, - `${uuid.toHexString()}.${fileExtension as string}`, + `${uuid.toHexString()}.${fileExtension}`, ); - await FileSystem.cp(file.uri, uri); + await FileSystem.cp(file.current.uri, uri); const { size } = await FileSystem.stat(uri); realm.write(() => { @@ -60,6 +76,7 @@ const AddMeme = ({ id: uuid, type: memeType, uri, + mimeType, size, title: memeTitle.parsed, isFavorite: memeIsFavorite, @@ -73,9 +90,7 @@ const AddMeme = ({ tag.memesLength = tag.memes.length; }); }); - - goBack(); - }; + }, [goBack, memeIsFavorite, memeTags, memeTitle.parsed, realm, storageUri]); return ( <> @@ -99,20 +114,46 @@ const AddMeme = ({ ]}> - + + diff --git a/src/screens/addTag.tsx b/src/screens/addTag.tsx index a567d04..f02848b 100644 --- a/src/screens/addTag.tsx +++ b/src/screens/addTag.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { ScrollView, View } from 'react-native'; +import React, { useCallback, useState } from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; import { Appbar, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { useRealm } from '@realm/react'; @@ -13,6 +13,17 @@ import { ORIENTATION, useDimensions } from '../contexts'; import { Tag } from '../database'; import { TagEditor } from '../components'; +const addTagStyles = StyleSheet.create({ + saveAndAddButton: { + flex: 1, + marginRight: 5, + }, + saveButton: { + flex: 1, + marginLeft: 5, + }, +}); + const AddTag = () => { const { goBack } = useNavigation(); const { colors } = useTheme(); @@ -24,16 +35,19 @@ const AddTag = () => { validateColor(generateRandomColor()), ); - const handleSave = () => { + // Although saving tags is instantaneous, we still want to show a loading + // indicator to prevent the user from spamming the save button. + const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] = + useState(false); + + const handleSave = useCallback(() => { realm.write(() => { realm.create(Tag.schema.name, { name: tagName.parsed, color: tagColor.parsed, }); }); - - goBack(); - }; + }, [realm, tagColor.parsed, tagName.parsed]); return ( <> @@ -59,12 +73,31 @@ const AddTag = () => { setTagColor={setTagColor} /> - + + diff --git a/src/screens/editMeme.tsx b/src/screens/editMeme.tsx index 029f45b..62ddad3 100644 --- a/src/screens/editMeme.tsx +++ b/src/screens/editMeme.tsx @@ -1,16 +1,15 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { ScrollView, View } from 'react-native'; import { Appbar, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useObject, useRealm } from '@realm/react'; -import { FileSystem } from 'react-native-file-access'; import { BSON } from 'realm'; import { ORIENTATION, useDimensions } from '../contexts'; import styles from '../styles'; import { RootStackParamList, ROUTE } from '../types'; import { Tag, Meme } from '../database'; -import { validateMemeTitle } from '../utilities'; +import { deleteMeme, favoriteMeme, validateMemeTitle } from '../utilities'; import { MemeEditor } from '../components'; const EditMeme = ({ @@ -34,9 +33,7 @@ const EditMeme = ({ const [isSaving, setIsSaving] = useState(false); - const handleSave = () => { - setIsSaving(true); - + const handleSave = useCallback(() => { realm.write(() => { meme.tags.forEach(tag => { if (!memeTags.has(tag.id.toHexString())) { @@ -60,32 +57,7 @@ const EditMeme = ({ meme.tagsLength = memeTags.size; meme.dateModified = new Date(); }); - - goBack(); - }; - - const handleFavorite = () => { - realm.write(() => { - meme.isFavorite = !meme.isFavorite; - }); - }; - - const handleDelete = async () => { - setIsSaving(true); - await FileSystem.unlink(meme.uri); - - realm.write(() => { - for (const tag of meme.tags) { - tag.dateModified = new Date(); - tag.memes.slice(tag.memes.indexOf(meme), 1); - tag.memesLength -= 1; - } - - realm.delete(meme); - }); - - goBack(); - }; + }, [meme, memeTags, memeTitle.parsed, realm]); return ( <> @@ -94,9 +66,17 @@ const EditMeme = ({ favoriteMeme(realm, meme)} + /> + { + setIsSaving(true); + await deleteMeme(realm, meme); + setIsSaving(false); + goBack(); + }} /> - { + setIsSaving(true); + handleSave(); + setIsSaving(false); + goBack(); + }} disabled={!memeTitle.valid || isSaving} loading={isSaving}> Save diff --git a/src/screens/editTag.tsx b/src/screens/editTag.tsx index da001c6..3a46155 100644 --- a/src/screens/editTag.tsx +++ b/src/screens/editTag.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { ScrollView, View } from 'react-native'; import { Appbar, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; @@ -10,7 +10,7 @@ import styles from '../styles'; import { ORIENTATION, useDimensions } from '../contexts'; import { ROUTE, RootStackParamList } from '../types'; import { Tag } from '../database'; -import { validateColor, validateTagName } from '../utilities'; +import { deleteTag, validateColor, validateTagName } from '../utilities'; const EditTag = ({ route, @@ -29,36 +29,26 @@ const EditTag = ({ const [tagName, setTagName] = useState(validateTagName(tag.name)); const [tagColor, setTagColor] = useState(validateColor(tag.color)); - const handleSave = () => { + const handleSave = useCallback(() => { realm.write(() => { tag.name = tagName.parsed; tag.color = tagColor.parsed; tag.dateModified = new Date(); }); - - goBack(); - }; - - const handleDelete = () => { - realm.write(() => { - for (const meme of tag.memes) { - meme.dateModified = new Date(); - meme.tags.slice(meme.tags.indexOf(tag), 1); - meme.tagsLength -= 1; - } - - realm.delete(tag); - }); - - goBack(); - }; + }, [realm, tag, tagColor.parsed, tagName.parsed]); return ( <> goBack()} /> - + { + deleteTag(realm, tag); + goBack(); + }} + /> { + handleSave(); + goBack(); + }} disabled={!tagName.valid || !tagColor.valid}> Save diff --git a/src/screens/index.ts b/src/screens/index.ts index 84a9511..8904357 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -3,6 +3,7 @@ export { default as AddTag } from './addTag'; export { default as EditMeme } from './editMeme'; export { default as EditTag } from './editTag'; export { default as Memes } from './memes'; +export { default as MemeView } from './memeView'; export { default as Settings } from './settings'; export { default as Tags } from './tags'; export { default as Welcome } from './welcome'; diff --git a/src/screens/memeView.tsx b/src/screens/memeView.tsx new file mode 100644 index 0000000..2f886f7 --- /dev/null +++ b/src/screens/memeView.tsx @@ -0,0 +1,136 @@ +import React, { useRef, useState } from 'react'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { StyleSheet, View } from 'react-native'; +import { useQuery, useRealm } from '@realm/react'; +import { FlashList } from '@shopify/flash-list'; +import { Appbar, Portal, Snackbar } from 'react-native-paper'; +import { RootStackParamList, ROUTE } from '../types'; +import { Meme } from '../database'; +import { useDimensions } from '../contexts'; +import { MemeViewItem } from '../components'; +import { + copyMeme, + deleteMeme, + editMeme, + favoriteMeme, + multipleIdQuery, + shareMeme, +} from '../utilities'; +import { NavigationProp, useNavigation } from '@react-navigation/native'; +import styles from '../styles'; + +const memeViewStyles = StyleSheet.create({ + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 80, + }, + snackbar: { + marginBottom: 90, + }, +}); + +const MemeView = ({ + route, +}: NativeStackScreenProps) => { + const { orientation, dimensions } = useDimensions(); + const navigation = useNavigation>(); + const realm = useRealm(); + + const { ids } = route.params; + const [index, setIndex] = useState(route.params.index); + + const [snackbarVisible, setSnackbarVisible] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + + const flashListRef = useRef>(null); + + const memes = useQuery(Meme.schema.name, collectionIn => { + return collectionIn.filtered(multipleIdQuery(ids)); + }); + + if (memes.length === 0) return <>; + + return ( + <> + + navigation.goBack()} /> + + + + { + const newIndex = Math.round( + event.nativeEvent.contentOffset.x / + event.nativeEvent.layoutMeasurement.width, + ); + if (newIndex !== index) setIndex(newIndex); + }} + estimatedItemSize={dimensions.width} + pagingEnabled + horizontal + showsHorizontalScrollIndicator={false} + estimatedListSize={{ + height: dimensions.height - 160, + width: dimensions.width, + }} + renderItem={({ item: meme }) => } + /> + + + favoriteMeme(realm, memes[index])} + /> + shareMeme(memes[index])} /> + { + copyMeme(memes[index]); + setSnackbarMessage('Meme copied!'); + setSnackbarVisible(true); + }} + /> + { + editMeme(navigation, memes[index]); + }} + /> + { + if (index === memes.length - 1) { + setIndex(index - 1); + flashListRef.current?.scrollToIndex({ + index: index - 1, + }); + } + void deleteMeme(realm, memes[index]); + if (memes.length === 1) navigation.goBack(); + }} + /> + + + setSnackbarVisible(false)} + style={memeViewStyles.snackbar} + action={{ + label: 'Dismiss', + onPress: () => setSnackbarVisible(false), + }}> + {snackbarMessage} + + + + ); +}; + +export default MemeView; diff --git a/src/screens/memes.tsx b/src/screens/memes.tsx index 462cf08..9312dd0 100644 --- a/src/screens/memes.tsx +++ b/src/screens/memes.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { BackHandler, NativeScrollEvent, @@ -9,71 +9,22 @@ import { useQuery } from '@realm/react'; import { useTheme } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; import { FlashList } from '@shopify/flash-list'; -import { useFocusEffect } from '@react-navigation/native'; +import { + ParamListBase, + useFocusEffect, + useNavigation, +} from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import styles from '../styles'; -import { SORT_DIRECTION, VIEW, memesSortQuery } from '../types'; +import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../types'; import { RootState, setNavVisible } from '../state'; import { Meme } from '../database'; -import { - HideableHeader, - MemesHeader, - MemesMasonryView, - MemesGridView, - MemesListView, -} from '../components'; - -const MemesView = ({ - memes, - flashListRef, - flashListPadding, - handleScroll, -}: { - memes: Realm.Results>; - flashListRef: RefObject>; - flashListPadding: number; - handleScroll: (event: NativeSyntheticEvent) => void; -}) => { - const view = useSelector((state: RootState) => state.memes.view); - - switch (view) { - case VIEW.MASONRY: { - return ( - - ); - } - case VIEW.GRID: { - return ( - - ); - } - case VIEW.LIST: { - return ( - - ); - } - default: { - return <>; - } - } -}; +import { HideableHeader, MemesHeader, MemesList } from '../components'; const Memes = () => { const { colors } = useTheme(); + const { navigate } = + useNavigation>(); const sort = useSelector((state: RootState) => state.memes.sort); const sortDirection = useSelector( (state: RootState) => state.memes.sortDirection, @@ -154,11 +105,9 @@ const Memes = () => { useFocusEffect( useCallback(() => { const handleBackPress = () => { - if (scrollOffset > 0) { - flashListRef.current?.scrollToOffset({ offset: 0, animated: true }); - return true; - } - return false; + if (scrollOffset <= 0) return false; + flashListRef.current?.scrollToOffset({ offset: 0, animated: true }); + return true; }; BackHandler.addEventListener('hardwareBackPress', handleBackPress); @@ -190,11 +139,17 @@ const Memes = () => { }} /> - { + navigate(ROUTE.MEME_VIEW, { + ids: memes.map(meme => meme.id.toHexString()), + index, + }); + }} /> ); diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index 2a65277..0a162d1 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -19,12 +19,12 @@ import { SORT_DIRECTION, tagSortQuery } from '../types'; import { ORIENTATION, useDimensions } from '../contexts'; const tagsStyles = StyleSheet.create({ - helperText: { - marginVertical: 10, - }, flashList: { paddingBottom: 100, }, + helperText: { + marginVertical: 15, + }, }); const Tags = () => { diff --git a/src/styles.tsx b/src/styles.tsx index 06ec455..49776e0 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -57,6 +57,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', }, + flexRowSpaceEvenly: { + flexDirection: 'row', + justifyContent: 'space-evenly', + }, flexColumn: { flexDirection: 'column', }, diff --git a/src/theme.tsx b/src/theme.ts similarity index 95% rename from src/theme.tsx rename to src/theme.ts index a6f0712..d80602c 100644 --- a/src/theme.tsx +++ b/src/theme.ts @@ -1,4 +1,7 @@ -import { DefaultTheme as DefaultNavigationTheme } from '@react-navigation/native'; +import { + DarkTheme, + DefaultTheme as LightTheme, +} from '@react-navigation/native'; import { MD3LightTheme, MD3DarkTheme, @@ -98,12 +101,12 @@ const darkTheme = { }; const { LightTheme: lightNavigationTheme } = adaptNavigationTheme({ - reactNavigationLight: DefaultNavigationTheme, + reactNavigationLight: LightTheme, materialLight: lightTheme, }); const { DarkTheme: darkNavigationTheme } = adaptNavigationTheme({ - reactNavigationDark: DefaultNavigationTheme, + reactNavigationDark: DarkTheme, materialDark: darkTheme, }); diff --git a/src/types/route.ts b/src/types/route.ts index 36bba4d..6815214 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -5,13 +5,19 @@ enum ROUTE { MEMES = 'Memes', TAGS = 'Tags', SETTINGS = 'Settings', + MEME_VIEW = 'Meme View', ADD_MEME = 'Add Meme', EDIT_MEME = 'Edit Meme', ADD_TAG = 'Add Tag', EDIT_TAG = 'Edit Tag', } -interface AddMemeRouteParamsFromFiles { +interface MemeViewRouteParams { + ids: string[]; + index: number; +} + +interface AddMemeRouteParams { file: DocumentPickerResponse; } @@ -26,11 +32,13 @@ interface EditTagRouteParams { interface RootStackParamList { [key: string]: | undefined - | AddMemeRouteParamsFromFiles + | MemeViewRouteParams + | AddMemeRouteParams | EditMemeRouteParams | EditTagRouteParams; [ROUTE.MAIN]: undefined; - [ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles; + [ROUTE.MEME_VIEW]: MemeViewRouteParams; + [ROUTE.ADD_MEME]: AddMemeRouteParams; [ROUTE.EDIT_MEME]: EditMemeRouteParams; [ROUTE.ADD_TAG]: undefined; [ROUTE.EDIT_TAG]: EditTagRouteParams; diff --git a/src/utilities/color.ts b/src/utilities/color.ts index 76ce8e4..c93e53a 100644 --- a/src/utilities/color.ts +++ b/src/utilities/color.ts @@ -32,6 +32,15 @@ const rgbToHex = (rgb: string) => { return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`; }; +const rgbToRgba = (rgb: string, alpha: number) => { + const [r, g, b] = rgb + .replaceAll(/[^\d,]/g, '') + .split(',') + .map(value => Number.parseInt(value, 10)); + + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; + const generateRandomColor = () => { const r = Math.floor(Math.random() * 256) .toString(16) @@ -50,5 +59,6 @@ export { isHexColor, isRgbColor, rgbToHex, + rgbToRgba, generateRandomColor, }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ca88de1..6029971 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -3,6 +3,7 @@ export { isHexColor, isRgbColor, rgbToHex, + rgbToRgba, generateRandomColor, } from './color'; export { packageName, appName, fileProvider, noOp } from './constants'; @@ -14,8 +15,16 @@ export { allowedMimeTypes, getMemeType, } from './filesystem'; -export { isPermissionForPath, clearPermissions } from './permissions'; export { getSortIcon, getViewIcon } from './icon'; +export { + favoriteMeme, + shareMeme, + copyMeme, + editMeme, + deleteMeme, +} from './meme'; +export { isPermissionForPath, clearPermissions } from './permissions'; +export { deleteTag } from './tag'; export { type StringValidationResult, validateMemeTitle, diff --git a/src/utilities/meme.ts b/src/utilities/meme.ts new file mode 100644 index 0000000..1ce1764 --- /dev/null +++ b/src/utilities/meme.ts @@ -0,0 +1,51 @@ +import { NavigationProp } from '@react-navigation/native'; +import { Dirs, FileSystem } from 'react-native-file-access'; +import { extension } from 'react-native-mime-types'; +import Share from 'react-native-share'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { Meme } from '../database'; +import { ROUTE, RootStackParamList } from '../types'; + +const favoriteMeme = (realm: Realm, meme: Meme) => { + realm.write(() => { + meme.isFavorite = !meme.isFavorite; + }); +}; + +const shareMeme = async (meme: Meme) => { + const fileExtension = extension(meme.mimeType) as string; + const cacheUri = `${Dirs.CacheDir}/${meme.id.toHexString()}.${fileExtension}`; + await FileSystem.cp(meme.uri, cacheUri); + await Share.open({ + url: `file://${cacheUri}`, + type: meme.mimeType, + failOnCancel: false, + }); +}; + +const copyMeme = (meme: Meme) => { + Clipboard.setURI(meme.uri); +}; + +const editMeme = ( + navigation: NavigationProp, + meme: Meme, +) => { + navigation.navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }); +}; + +const deleteMeme = async (realm: Realm, meme: Meme) => { + await FileSystem.unlink(meme.uri); + + realm.write(() => { + for (const tag of meme.tags) { + tag.dateModified = new Date(); + tag.memes.slice(tag.memes.indexOf(meme), 1); + tag.memesLength -= 1; + } + + realm.delete(meme); + }); +}; + +export { favoriteMeme, shareMeme, copyMeme, editMeme, deleteMeme }; diff --git a/src/utilities/tag.ts b/src/utilities/tag.ts new file mode 100644 index 0000000..5bdc5ec --- /dev/null +++ b/src/utilities/tag.ts @@ -0,0 +1,16 @@ +import { Realm } from '@realm/react'; +import { Tag } from '../database'; + +const deleteTag = (realm: Realm, tag: Tag) => { + realm.write(() => { + for (const meme of tag.memes) { + meme.dateModified = new Date(); + meme.tags.slice(meme.tags.indexOf(tag), 1); + meme.tagsLength -= 1; + } + + realm.delete(tag); + }); +}; + +export { deleteTag };