Add meme view & sharing

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-24 21:55:36 +03:00
parent 04661ca356
commit e479e3c0ad
33 changed files with 724 additions and 482 deletions

View File

@@ -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<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
const gridColumns = useSelector(
(state: RootState) => state.settings.gridColumns,
);
return (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(gridColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={gridColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesGridItem meme={meme} />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...gridViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[gridViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesGridView;

View File

@@ -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';

View File

@@ -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<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
return (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={50}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesListItem meme={meme} />}
ItemSeparatorComponent={() => <Divider />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...gridViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[gridViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesListView;

View File

@@ -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<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns,
);
return (
<MasonryFlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(masonryColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={masonryColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesMasonryItem meme={meme} />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...memeMasonryViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memeMasonryViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesMasonryView;

View File

@@ -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<string, Tag>;
@@ -38,15 +38,23 @@ const MemeEditor = ({
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
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 <LoadingView />;
@@ -64,7 +72,7 @@ const MemeEditor = ({
{memeTitle.error}
</HelperText>
<Image
source={{ uri: imageUri }}
source={{ uri: memeUri }}
style={[
{
width: imageWidth,

View File

@@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, useTheme } from 'react-native-paper';
import { ImageZoom } from '@likashefqet/react-native-image-zoom';
import { Meme } from '../../database';
import { useDimensions } from '../../contexts';
import { Image, View } from 'react-native';
import styles from '../../styles';
const MemeViewItem = ({ meme }: { meme: Meme }) => {
const { dimensions } = useDimensions();
const { colors } = useTheme();
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
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 (
<View
style={[
{
width: dimensions.width,
height: dimensions.height - 160,
backgroundColor: colors.background,
},
styles.centered,
]}>
{imageWidth && imageHeight ? (
<ImageZoom
source={{ uri: meme.uri }}
style={{ width: imageWidth, height: imageHeight }}
/>
) : (
<ActivityIndicator size="large" color={colors.primary} />
)}
</View>
);
};
export default MemeViewItem;

View File

@@ -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<NavigationProp<RootStackParamList>>();
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 && (
<View>
<TouchableHighlight
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}>
<TouchableHighlight onPress={() => focusMeme(index)}>
<Image
source={{ uri: meme.uri }}
style={[{ width: imageWidth, height: imageHeight }]}

View File

@@ -0,0 +1,167 @@
import React, { RefObject } from 'react';
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
import {
NativeSyntheticEvent,
NativeScrollEvent,
StyleSheet,
} from 'react-native';
import { useSelector } from 'react-redux';
import { Divider, HelperText } from 'react-native-paper';
import { useDimensions, ORIENTATION } from '../../../contexts';
import { Meme } from '../../../database';
import { RootState } from '../../../state';
import { VIEW } from '../../../types';
import { getFlashListItemHeight } from '../../../utilities';
import styles from '../../../styles';
import MemesMasonryItem from './memesMasonryItem';
import MemesGridItem from './memesGridItem';
import MemesListItem from './memesListItem';
const memesMasonryListStyles = StyleSheet.create({
flashList: {
paddingBottom: 100,
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
paddingHorizontal: 0.1,
},
helperText: {
marginVertical: 15,
},
});
const memesGridListStyles = StyleSheet.create({
flashList: {
paddingBottom: 100,
paddingHorizontal: 2.5,
},
helperText: {
marginVertical: 12.5,
},
});
const memesListListStyles = StyleSheet.create({
flashList: {
paddingBottom: 100,
paddingHorizontal: 5,
},
helperText: {
marginVertical: 15,
},
});
const MemesList = ({
memes,
flashListRef,
flashListPadding,
handleScroll,
focusMeme,
}: {
memes: Realm.Results<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => 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 && (
<MasonryFlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(masonryColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={masonryColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => (
<MemesMasonryItem meme={meme} index={index} focusMeme={focusMeme} />
)}
contentContainerStyle={{
paddingTop: extraFlashListPadding,
...memesMasonryListStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memesMasonryListStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
)}
{view === VIEW.GRID && (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(gridColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={gridColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => (
<MemesGridItem meme={meme} index={index} focusMeme={focusMeme} />
)}
contentContainerStyle={{
paddingTop: extraFlashListPadding + 2.5,
...memesGridListStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memesGridListStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
)}
{view === VIEW.LIST && (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={50}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => (
<MemesListItem meme={meme} index={index} focusMeme={focusMeme} />
)}
ItemSeparatorComponent={() => <Divider />}
contentContainerStyle={{
paddingTop: extraFlashListPadding,
...memesListListStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memesListListStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
)}
</>
);
};
export default MemesList;

View File

@@ -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<NavigationProp<RootStackParamList>>();
const MemesListItem = ({
meme,
index,
focusMeme,
}: {
meme: Meme;
index: number;
focusMeme: (index: number) => void;
}) => {
const { dimensions } = useDimensions();
const [imageWidth, setImageWidth] = useState<number>();
@@ -40,9 +45,7 @@ const MemesListItem = ({ meme }: { meme: Meme }) => {
<>
{imageWidth && imageHeight && (
<TouchableRipple
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
onPress={() => focusMeme(index)}
style={[memesListItemStyles.view, styles.flexRow]}>
<>
<View style={{ width: imageWidth, height: imageHeight }}>

View File

@@ -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<NavigationProp<RootStackParamList>>();
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 && (
<TouchableHighlight
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
onPress={() => focusMeme(index)}
style={memeMasonryItemStyles.view}>
<Image
source={{ uri: meme.uri }}