Add memes & meme-editing views

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-22 17:13:16 +03:00
parent fa0a89f324
commit e44ee7de34
16 changed files with 298 additions and 101 deletions

View File

@@ -3,7 +3,7 @@ import { Keyboard, StyleSheet } from 'react-native';
import { FAB } from 'react-native-paper'; import { FAB } from 'react-native-paper';
import { ParamListBase, useNavigation } from '@react-navigation/native'; import { ParamListBase, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { pick } from 'react-native-document-picker'; import { pickSingle } from 'react-native-document-picker';
import { ORIENTATION, useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { ROUTE } from '../types'; import { ROUTE } from '../types';
import { allowedMimeTypes, noOp } from '../utilities'; import { allowedMimeTypes, noOp } from '../utilities';
@@ -65,9 +65,9 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
onStateChange={({ open }) => setState(open)} onStateChange={({ open }) => setState(open)}
onPress={async () => { onPress={async () => {
if (!state) return; if (!state) return;
const res = await pick({ type: allowedMimeTypes }).catch(noOp); const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp);
if (!res) return; if (!file) return;
navigate(ROUTE.ADD_MEME, { uri: res }); navigate(ROUTE.ADD_MEME, { file });
}} }}
style={ style={
orientation === ORIENTATION.PORTRAIT orientation === ORIENTATION.PORTRAIT

View File

@@ -0,0 +1,51 @@
import React, { useState } from 'react';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Meme } from '../../database';
import { ROUTE, RootStackParamList } from '../../types';
import { Card } from 'react-native-paper';
import { Image, StyleSheet } from 'react-native';
import { useDimensions } from '../../contexts';
const memeCardStyles = StyleSheet.create({
card: {
margin: 5,
},
});
const MemeCard = ({ meme }: { meme: Meme }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const { dimensions } = useDimensions();
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, (width, height) => {
const paddedWidth = (dimensions.width * 0.92) / 2 - 10;
setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height);
});
return (
<>
{imageWidth && imageHeight && (
<Card
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
style={memeCardStyles.card}>
<Card.Cover
source={{ uri: meme.uri }}
style={{ width: imageWidth, height: imageHeight }}
/>
<Card.Title
title={meme.title}
titleVariant="titleSmall"
titleNumberOfLines={3}
/>
</Card>
)}
</>
);
};
export default MemeCard;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { HelperText, TextInput } from 'react-native-paper'; import { HelperText, TextInput } from 'react-native-paper';
import { Image } from 'react-native'; import { Image } from 'react-native';
import { useDimensions } from '../../contexts'; import { useDimensions } from '../../contexts';
@@ -46,13 +46,11 @@ const MemeEditor = ({
const [imageWidth, setImageWidth] = useState<number>(); const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>(); const [imageHeight, setImageHeight] = useState<number>();
useEffect(() => { Image.getSize(imageUri, (width, height) => {
Image.getSize(imageUri, (width, height) => { const paddedWidth = dimensions.width * 0.92;
const paddedWidth = dimensions.width * 0.92; setImageWidth(paddedWidth);
setImageWidth(paddedWidth); setImageHeight((paddedWidth / width) * height);
setImageHeight((paddedWidth / width) * height); });
});
}, [dimensions.width, imageUri]);
if (!imageWidth || !imageHeight) return <LoadingView />; if (!imageWidth || !imageHeight) return <LoadingView />;

View File

@@ -59,7 +59,7 @@ const MemeTagSearchModal = ({
let collection = collectionIn; let collection = collectionIn;
if (search) { if (search) {
collection = collection.filtered('name CONTAINS[c] $0', search); collection = collection.filtered('name CONTAINS[c] $0', tagName.parsed);
} }
collection = collection.sorted( collection = collection.sorted(
@@ -88,7 +88,6 @@ const MemeTagSearchModal = ({
if (!tag) return; if (!tag) return;
memeTags.set(tag.id.toHexString(), tag); memeTags.set(tag.id.toHexString(), tag);
setMemeTags(new Map(memeTags)); setMemeTags(new Map(memeTags));
setSearch(tag.name);
}; };
return ( return (

View File

@@ -22,8 +22,9 @@ const TagEditor = ({
const lastValidTagColor = useRef(tagColor.parsed); const lastValidTagColor = useRef(tagColor.parsed);
const handleTagColorChange = (color: string) => { const handleTagColorChange = (color: string) => {
setTagColor(validateColor(color)); const result = validateColor(color);
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed; setTagColor(result);
if (result.valid) lastValidTagColor.current = result.parsed;
}; };
return ( return (

View File

@@ -21,13 +21,12 @@ const memeTypePlural = {
class Meme extends Object<Meme> { class Meme extends Object<Meme> {
id!: BSON.UUID; id!: BSON.UUID;
type!: MEME_TYPE; type!: MEME_TYPE;
uri!: string[]; uri!: string;
hash!: string[];
size!: number; size!: number;
title!: string; title!: string;
description?: string; description!: string;
isFavorite!: boolean; isFavorite!: boolean;
tags!: Tag[] | Set<Tag>; tags!: Tag[] | Realm.Set<Tag>;
tagsLength!: number; tagsLength!: number;
dateCreated!: Date; dateCreated!: Date;
dateModified!: Date; dateModified!: Date;
@@ -40,11 +39,10 @@ class Meme extends Object<Meme> {
properties: { properties: {
id: { type: 'uuid', default: () => new BSON.UUID() }, id: { type: 'uuid', default: () => new BSON.UUID() },
type: { type: 'string', indexed: true }, type: { type: 'string', indexed: true },
uri: 'string[]', uri: 'string',
hash: 'string[]',
size: 'int', size: 'int',
title: 'string', title: 'string',
description: 'string?', description: { type: 'string', default: '' },
isFavorite: { type: 'bool', indexed: true, default: false }, isFavorite: { type: 'bool', indexed: true, default: false },
tags: { type: 'set', objectType: 'Tag', default: [] }, tags: { type: 'set', objectType: 'Tag', default: [] },
tagsLength: { type: 'int', default: 0 }, tagsLength: { type: 'int', default: 0 },

View File

@@ -7,7 +7,7 @@ class Tag extends Object<Tag> {
id!: BSON.UUID; id!: BSON.UUID;
name!: string; name!: string;
color!: string; color!: string;
memes!: Meme[] | Set<Meme>; memes!: Meme[] | Realm.Set<Meme>;
memesLength!: number; memesLength!: number;
dateCreated!: Date; dateCreated!: Date;
dateModified!: Date; dateModified!: Date;

View File

@@ -23,7 +23,7 @@ import { MemeEditor } from '../components';
const AddMeme = ({ const AddMeme = ({
route, route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
const navigation = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const { orientation } = useDimensions();
const realm = useRealm(); const realm = useRealm();
@@ -32,9 +32,7 @@ const AddMeme = ({
(state: RootState) => state.settings.storageUri, (state: RootState) => state.settings.storageUri,
)!; )!;
const uri = route.params.uri[0].uri; const { file } = route.params;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const memeType = getMemeType(route.params.uri[0].type!);
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
const [memeDescription, setMemeDescription] = useState( const [memeDescription, setMemeDescription] = useState(
@@ -49,31 +47,27 @@ const AddMeme = ({
setIsSaving(true); setIsSaving(true);
const uuid = new BSON.UUID(); const uuid = new BSON.UUID();
const savedUri: string[] = [];
const hash: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fileExtension = extension(memeType!); const mimeType = file.type!;
if (!fileExtension) navigation.goBack(); const memeType = getMemeType(mimeType);
savedUri.push( const fileExtension = extension(mimeType);
AndroidScoped.appendPath( if (!fileExtension) goBack();
storageUri,
`${uuid.toHexString()}.${fileExtension as string}`, const uri = AndroidScoped.appendPath(
), storageUri,
`${uuid.toHexString()}.${fileExtension as string}`,
); );
await FileSystem.cp(uri, savedUri[0]); await FileSystem.cp(file.uri, uri);
const { size } = await FileSystem.stat(savedUri[0]); const { size } = await FileSystem.stat(uri);
hash.push(await FileSystem.hash(savedUri[0], 'MD5'));
realm.write(() => { realm.write(() => {
const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, { const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, {
id: uuid, id: uuid,
type: memeType, type: memeType,
uri: savedUri, uri,
size, size,
hash,
title: memeTitle.parsed, title: memeTitle.parsed,
description: memeDescription.parsed, description: memeDescription.parsed,
isFavorite: memeIsFavorite, isFavorite: memeIsFavorite,
@@ -81,22 +75,21 @@ const AddMeme = ({
tagsLength: memeTags.size, tagsLength: memeTags.size,
}); });
meme.tags.forEach(tag => { memeTags.forEach(tag => {
tag.dateModified = new Date(); tag.dateModified = new Date();
const memes = tag.memes as Set<Meme>; const memes = tag.memes as Realm.Set<Meme>;
memes.add(meme); memes.add(meme);
tag.memesLength = memes.size; tag.memesLength = memes.size;
}); });
}); });
setIsSaving(false); goBack();
navigation.goBack();
}; };
return ( return (
<> <>
<Appbar.Header> <Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => goBack()} />
<Appbar.Content title={'Add Meme'} /> <Appbar.Content title={'Add Meme'} />
<Appbar.Action <Appbar.Action
icon={memeIsFavorite ? 'heart' : 'heart-outline'} icon={memeIsFavorite ? 'heart' : 'heart-outline'}
@@ -115,7 +108,7 @@ const AddMeme = ({
]}> ]}>
<View style={[styles.flex, styles.justifyStart]}> <View style={[styles.flex, styles.justifyStart]}>
<MemeEditor <MemeEditor
imageUri={uri} imageUri={file.uri}
memeTitle={memeTitle} memeTitle={memeTitle}
setMemeTitle={setMemeTitle} setMemeTitle={setMemeTitle}
memeDescription={memeDescription} memeDescription={memeDescription}

View File

@@ -14,7 +14,7 @@ import { Tag } from '../database';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
const AddTag = () => { const AddTag = () => {
const navigation = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const { orientation } = useDimensions();
const realm = useRealm(); const realm = useRealm();
@@ -32,13 +32,13 @@ const AddTag = () => {
}); });
}); });
navigation.goBack(); goBack();
}; };
return ( return (
<> <>
<Appbar.Header> <Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => goBack()} />
<Appbar.Content title={'Add Tag'} /> <Appbar.Content title={'Add Tag'} />
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView

View File

@@ -1,20 +1,112 @@
import React from 'react'; import React, { useState } from 'react';
import { ScrollView } from 'react-native'; import { ScrollView, View } from 'react-native';
import { Appbar, useTheme } from 'react-native-paper'; import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; 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 { ORIENTATION, useDimensions } from '../contexts';
import styles from '../styles'; import styles from '../styles';
import { RootStackParamList, ROUTE } from '../types';
import { Tag, Meme } from '../database';
import { validateMemeTitle, validateMemeDescription } from '../utilities';
import { MemeEditor } from '../components';
const EditMeme = () => { const EditMeme = ({
const navigation = useNavigation(); route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => {
const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const { orientation } = useDimensions();
const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const meme = useObject<Meme>(
Meme.schema.name,
BSON.UUID.createFromHexString(route.params.id),
)!;
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
const [memeDescription, setMemeDescription] = useState(
validateMemeDescription(meme.description),
);
const [memeIsFavorite, setMemeIsFavorite] = useState(meme.isFavorite);
const [memeTags, setMemeTags] = useState(
new Map<string, Tag>(
(meme.tags as Realm.Set<Tag>).map(tag => [tag.id.toHexString(), tag]),
),
);
const [isSaving, setIsSaving] = useState(false);
const handleSave = () => {
setIsSaving(true);
realm.write(() => {
meme.tags.forEach(tag => {
if (!memeTags.has(tag.id.toHexString())) {
const memes = tag.memes as Realm.Set<Meme>;
memes.delete(meme);
tag.memesLength = memes.size;
tag.dateModified = new Date();
}
});
memeTags.forEach(tag => {
if (!(meme.tags as Realm.Set<Tag>).has(tag)) {
const memes = tag.memes as Realm.Set<Meme>;
memes.add(meme);
tag.memesLength = memes.size;
tag.dateModified = new Date();
}
});
meme.title = memeTitle.parsed;
meme.description = memeDescription.parsed;
meme.tags = [...memeTags.values()];
meme.tagsLength = memeTags.size;
meme.dateModified = new Date();
});
goBack();
};
const handleFavorite = () => {
realm.write(() => {
meme.isFavorite = !memeIsFavorite;
});
setMemeIsFavorite(!memeIsFavorite);
};
const handleDelete = async () => {
setIsSaving(true);
await FileSystem.unlink(meme.uri);
realm.write(() => {
for (const tag of meme.tags) {
tag.dateModified = new Date();
const memes = tag.memes as Realm.Set<Meme>;
memes.delete(meme);
tag.memesLength = memes.size;
}
realm.delete(meme);
});
goBack();
};
return ( return (
<> <>
<Appbar.Header> <Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => goBack()} />
<Appbar.Content title={'Edit Meme'} /> <Appbar.Content title={'Edit Meme'} />
<Appbar.Action
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
onPress={handleFavorite}
/>
<Appbar.Action icon="delete" onPress={handleDelete} />
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
@@ -25,7 +117,29 @@ const EditMeme = () => {
styles.flexGrow, styles.flexGrow,
styles.flexColumnSpaceBetween, styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
]}></ScrollView> ]}>
<View style={[styles.flex, styles.justifyStart]}>
<MemeEditor
imageUri={meme.uri}
memeTitle={memeTitle}
setMemeTitle={setMemeTitle}
memeDescription={memeDescription}
setMemeDescription={setMemeDescription}
memeTags={memeTags}
setMemeTags={setMemeTags}
/>
</View>
<View style={[styles.flex, styles.justifyEnd]}>
<Button
mode="contained"
icon="floppy"
onPress={handleSave}
disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
loading={isSaving}>
Save
</Button>
</View>
</ScrollView>
</> </>
); );
}; };

View File

@@ -4,7 +4,7 @@ import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { useRealm } from '@realm/react'; import { useObject, useRealm } from '@realm/react';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
import styles from '../styles'; import styles from '../styles';
import { ORIENTATION, useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
@@ -15,14 +15,14 @@ import { validateColor, validateTagName } from '../utilities';
const EditTag = ({ const EditTag = ({
route, route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => {
const navigation = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const { orientation } = useDimensions();
const realm = useRealm(); const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tag = realm.objectForPrimaryKey( const tag = useObject<Tag>(
Tag, Tag.schema.name,
BSON.UUID.createFromHexString(route.params.id), BSON.UUID.createFromHexString(route.params.id),
)!; )!;
@@ -36,14 +36,14 @@ const EditTag = ({
tag.dateModified = new Date(); tag.dateModified = new Date();
}); });
navigation.goBack(); goBack();
}; };
const handleDelete = () => { const handleDelete = () => {
realm.write(() => { realm.write(() => {
for (const meme of tag.memes) { for (const meme of tag.memes) {
meme.dateModified = new Date(); meme.dateModified = new Date();
const tags = meme.tags as Set<Tag>; const tags = meme.tags as Realm.Set<Tag>;
tags.delete(tag); tags.delete(tag);
meme.tagsLength = tags.size; meme.tagsLength = tags.size;
} }
@@ -51,13 +51,13 @@ const EditTag = ({
realm.delete(tag); realm.delete(tag);
}); });
navigation.goBack(); goBack();
}; };
return ( return (
<> <>
<Appbar.Header> <Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => goBack()} />
<Appbar.Content title={'Edit Tag'} /> <Appbar.Content title={'Edit Tag'} />
<Appbar.Action icon="delete" onPress={handleDelete} /> <Appbar.Action icon="delete" onPress={handleDelete} />
</Appbar.Header> </Appbar.Header>

View File

@@ -9,29 +9,34 @@ import {
import { useQuery } from '@realm/react'; import { useQuery } from '@realm/react';
import { useTheme, HelperText } from 'react-native-paper'; import { useTheme, HelperText } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list'; import { FlashList, MasonryFlashList } from '@shopify/flash-list';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import styles from '../styles'; import styles from '../styles';
import { SORT_DIRECTION, memesSortQuery } from '../types'; import { SORT_DIRECTION, memesSortQuery } from '../types';
import { RootState, setNavVisible } from '../state'; import { RootState, setNavVisible } from '../state';
import { Meme, Tag } from '../database'; import { Meme } from '../database';
import { useDimensions } from '../contexts'; import { ORIENTATION, useDimensions } from '../contexts';
import { HideableHeader, MemesHeader } from '../components'; import { HideableHeader, MemesHeader } from '../components';
import MemeCard from '../components/memes/memeCard';
const memesStyles = StyleSheet.create({ const memesStyles = StyleSheet.create({
helperText: { helperText: {
marginVertical: 10, marginVertical: 10,
}, },
flashList: {
paddingBottom: 100,
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
paddingHorizontal: 0.01,
},
}); });
const Memes = () => { const Memes = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const { dimensions, orientation } = useDimensions();
const sort = useSelector((state: RootState) => state.memes.sort); const sort = useSelector((state: RootState) => state.memes.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.memes.sortDirection, (state: RootState) => state.memes.sortDirection,
); );
const view = useSelector((state: RootState) => state.memes.view);
const favoritesOnly = useSelector( const favoritesOnly = useSelector(
(state: RootState) => state.memes.favoritesOnly, (state: RootState) => state.memes.favoritesOnly,
); );
@@ -80,10 +85,11 @@ const Memes = () => {
setScrollOffset(currentOffset); setScrollOffset(currentOffset);
}; };
const flashListRef = useRef<FlashList<Tag>>(null); const flashListRef = useRef<FlashList<Meme>>(null);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
dispatch(setNavVisible(true));
const handleBackPress = () => { const handleBackPress = () => {
if (scrollOffset > 0) { if (scrollOffset > 0) {
flashListRef.current?.scrollToOffset({ offset: 0, animated: true }); flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
@@ -96,7 +102,7 @@ const Memes = () => {
return () => return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress); BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [flashListRef, scrollOffset]), }, [dispatch, scrollOffset]),
); );
return ( return (
@@ -115,6 +121,33 @@ const Memes = () => {
}} }}
/> />
</HideableHeader> </HideableHeader>
<MasonryFlashList
ref={flashListRef}
data={memes}
estimatedItemSize={200}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={2}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemeCard meme={meme} />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...memesStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memesStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
</View> </View>
); );
}; };

View File

@@ -80,6 +80,7 @@ const Tags = () => {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
dispatch(setNavVisible(true));
const handleBackPress = () => { const handleBackPress = () => {
if (scrollOffset > 0) { if (scrollOffset > 0) {
flashListRef.current?.scrollToOffset({ offset: 0, animated: true }); flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
@@ -92,7 +93,7 @@ const Tags = () => {
return () => return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress); BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [flashListRef, scrollOffset]), }, [dispatch, scrollOffset]),
); );
return ( return (
@@ -111,31 +112,33 @@ const Tags = () => {
}} }}
/> />
</HideableHeader> </HideableHeader>
{flashListPadding > 0 && ( <FlashList
<FlashList ref={flashListRef}
ref={flashListRef} data={tags}
data={tags} estimatedItemSize={50}
estimatedItemSize={52} estimatedListSize={{
showsVerticalScrollIndicator={false} height: dimensions.height,
renderItem={({ item: tag }) => <TagRow tag={tag} />} width: dimensions.width * 0.92,
contentContainerStyle={{ }}
paddingTop: showsVerticalScrollIndicator={false}
flashListPadding + renderItem={({ item: tag }) => <TagRow tag={tag} />}
dimensions.height * contentContainerStyle={{
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), paddingTop:
...tagsStyles.flashList, flashListPadding +
}} dimensions.height *
ItemSeparatorComponent={() => <Divider />} (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
ListEmptyComponent={() => ( ...tagsStyles.flashList,
<HelperText }}
type={'info'} ItemSeparatorComponent={() => <Divider />}
style={[tagsStyles.helperText, styles.centerText]}> ListEmptyComponent={() => (
No tags found <HelperText
</HelperText> type={'info'}
)} style={[tagsStyles.helperText, styles.centerText]}>
onScroll={handleScroll} No tags found
/> </HelperText>
)} )}
onScroll={handleScroll}
/>
</View> </View>
); );
}; };

6
src/types/dimensions.ts Normal file
View File

@@ -0,0 +1,6 @@
interface Dimensions {
width: number;
height: number;
}
export { type Dimensions };

View File

@@ -1,3 +1,4 @@
export { type Dimensions } from './dimensions';
export { ROUTE, type RootStackParamList } from './route'; export { ROUTE, type RootStackParamList } from './route';
export { export {
MEME_SORT, MEME_SORT,

View File

@@ -12,7 +12,7 @@ enum ROUTE {
} }
interface AddMemeRouteParamsFromFiles { interface AddMemeRouteParamsFromFiles {
uri: DocumentPickerResponse[]; file: DocumentPickerResponse;
} }
interface EditMemeRouteParams { interface EditMemeRouteParams {