Feature / recently used emojis (#14)

* feat: add translations to recently used

* fix: sort order

* feat: add recently used emojis

* docs: add examples

* refactor: change hideRecentlyUsed to enableRecentlyUsed

* docs: add enableRecentlyUsed to docs

* fix: delete unnecessary arrow

* fix: from code review
This commit is contained in:
Jakub Grzywacz 2021-07-30 12:53:00 +02:00 committed by GitHub
parent c0cbef73c2
commit de2d2f3e0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 105 additions and 23 deletions

View File

@ -45,6 +45,8 @@ export default function App() {
| open | boolean | false | yes | Opens modal picker |
| onClose | function | undefined | yes | Request close modal *runs when onEmojiSelected or backdrop pressed* |
| emojiSize | number | 28 | no | Custom emoji size |
| enableRecentlyUsed | boolean | false | no | Enable recently used emojis in categories |
| categoryPosition | 'floating' \| 'top' \| 'bottom' | 'floating' | no | Specify category container position |
| headerStyles | TextStyle | {} | no | Override category name styles |
| knobStyles | ViewStyle | {} | no | Override knob styles |
| containerStyles | ViewStyle | {} | no | Override container styles |
@ -60,7 +62,6 @@ export default function App() {
| onCategoryChangeFailed | function | warn(info) | no | Callback on category change failed (info: {index, highestMeasuredFrameIndex, averageItemLength}) |
| translation | CategoryTranslation | en | no | Translation object *see translation section* |
| disabledCategory | CategoryTypes[] | [] | no | Hide categories by passing their slugs |
| categoryPosition | CategoryPosition | categoryPosition | no | Specify category container position |
## 📊 Comparison
@ -119,6 +120,8 @@ You can clone the repo and run `yarn example ios` or `yarn example android` to p
![Preview](/example/assets/static-modal-preview.jpg)
### [Static](/example/src/Static/Static.tsx)
![Preview](/example/assets/static-preview.jpg)
### [Recently used](/example/src/EnableRecently/EnableRecently.tsx)
![Preview](/example/assets/enable-recently-used-preview.jpg)
### [Categories Top](/example/src/TopCategory/TopCategory.tsx)
![Preview](/example/assets/categories-top-preview.jpg)
### [Categories Bottom](/example/src/BottomCategory/BottomCategory.tsx)

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -9,6 +9,7 @@ import Translated from './Translated/Translated';
import DisabledCategories from './DisabledCategories/DisabledCategories';
import StaticModal from './StaticModal/StaticModal';
import Static from './Static/Static';
import EnableRecently from './EnableRecently/EnableRecently';
import TopCategory from './TopCategory/TopCategory';
import BottomCategory from './BottomCategory/BottomCategory';
@ -19,7 +20,10 @@ export default () => {
<Stack.Navigator>
<Stack.Screen name="Examples" component={Examples} />
<Stack.Screen name="Basic" component={Basic} />
<Stack.Screen name="EnableRecently" component={EnableRecently} />
<Stack.Screen name="Dark" component={Dark} />
<Stack.Screen name="TopCategory" component={TopCategory} />
<Stack.Screen name="BottomCategory" component={BottomCategory} />
<Stack.Screen name="Translated" component={Translated} />
<Stack.Screen
name="DisabledCategories"
@ -27,8 +31,6 @@ export default () => {
/>
<Stack.Screen name="StaticModal" component={StaticModal} />
<Stack.Screen name="Static" component={Static} />
<Stack.Screen name="TopCategory" component={TopCategory} />
<Stack.Screen name="BottomCategory" component={BottomCategory} />
</Stack.Navigator>
</NavigationContainer>
);

View File

@ -0,0 +1,44 @@
import * as React from 'react';
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import EmojiPicker from 'rn-emoji-keyboard';
import type { EmojiType } from 'src/types';
const EnableRecently = () => {
const [result, setResult] = React.useState<string>();
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
const handlePick = (emoji: EmojiType) => {
console.log(emoji);
setResult(emoji.emoji);
setIsModalOpen((prev) => !prev);
};
return (
<SafeAreaView style={styles.container}>
<Text style={styles.text}>Result: {result}</Text>
<TouchableOpacity onPress={() => setIsModalOpen(true)}>
<Text style={styles.text}>Open</Text>
</TouchableOpacity>
<EmojiPicker
onEmojiSelected={handlePick}
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
enableRecentlyUsed
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
text: {
textAlign: 'center',
margin: 64,
fontSize: 18,
},
});
export default EnableRecently;

View File

@ -11,6 +11,7 @@ type RootStackParamList = {
DisabledCategories: undefined;
StaticModal: undefined;
Static: undefined;
EnableRecently: undefined;
TopCategory: undefined;
BottomCategory: undefined;
};
@ -39,6 +40,10 @@ const Examples = ({ navigation }: Props) => {
title="Static Component"
onPress={() => navigation.navigate('Static')}
/>
<Button
title="Enable recently used"
onPress={() => navigation.navigate('EnableRecently')}
/>
<Button
title="Category Top"
onPress={() => navigation.navigate('TopCategory')}

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { View, Animated, StyleSheet, FlatList, ViewStyle } from 'react-native';
import { useKeyboardStore } from '../store/useKeyboardStore';
import { defaultKeyboardContext } from '../contexts/KeyboardProvider';
import { KeyboardContext } from '../contexts/KeyboardContext';
import {
@ -22,9 +23,10 @@ export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
onCategoryChangeFailed,
disabledCategory,
activeCategoryContainerColor,
enableRecentlyUsed,
categoryPosition,
} = React.useContext(KeyboardContext);
const { keyboardState } = useKeyboardStore();
const handleScrollToCategory = React.useCallback(
(category: CategoryTypes) => {
flatListRef?.current?.scrollToIndex({
@ -63,6 +65,9 @@ export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
),
[activeCategoryContainerColor, scrollNav]
);
const isRecentlyUsedHidden = (category: CategoryTypes) =>
category === 'recently_used' &&
(keyboardState.recentlyUsed.length === 0 || !enableRecentlyUsed);
const getStylesBasedOnPosition = () => {
const style: ViewStyle[] = [styles.navigation];
@ -94,9 +99,10 @@ export const Categories = ({ flatListRef, scrollNav }: CategoriesProps) => {
<View style={[categoryPosition === 'floating' && styles.floating]}>
<View style={getStylesBasedOnPosition()}>
<FlatList
data={CATEGORIES_NAVIGATION.filter(
({ category }) => !disabledCategory.includes(category)
)}
data={CATEGORIES_NAVIGATION.filter(({ category }) => {
if (isRecentlyUsedHidden(category)) return false;
return !disabledCategory.includes(category);
})}
keyExtractor={(item) => item.category}
renderItem={rendarItem}
ItemSeparatorComponent={() => <View style={styles.separator} />}

View File

@ -60,7 +60,7 @@ export const EmojiCategory = ({
if (emoji.name === 'blank emoji') return;
const parsedEmoji = parseEmoji(emoji);
onEmojiSelected(parsedEmoji);
setKeyboardState({ type: 'RECENT_EMOJI_ADD', payload: parsedEmoji });
setKeyboardState({ type: 'RECENT_EMOJI_ADD', payload: emoji });
},
[onEmojiSelected, setKeyboardState]
);

View File

@ -12,6 +12,7 @@ import { EmojiCategory } from './EmojiCategory';
import { KeyboardContext } from '../contexts/KeyboardContext';
import { Categories } from './Categories';
import emojisByGroup from '../assets/emojis.json';
import { useKeyboardStore } from '../store/useKeyboardStore';
export const EmojiStaticKeyboard = () => {
const { width } = useWindowDimensions();
@ -22,9 +23,8 @@ export const EmojiStaticKeyboard = () => {
disabledCategory,
categoryPosition,
} = React.useContext(KeyboardContext);
const { keyboardState } = useKeyboardStore();
const flatListRef = React.useRef<FlatList>(null);
const scrollNav = React.useRef(new Animated.Value(0)).current;
const getItemLayout = (
@ -47,6 +47,20 @@ export const EmojiStaticKeyboard = () => {
}).start();
}, [activeCategoryIndex, scrollNav]);
const renderList = React.useMemo(() => {
const data = emojisByGroup.filter((category) => {
const title = category.title as CategoryTypes;
return !disabledCategory.includes(title);
});
if (keyboardState.recentlyUsed.length) {
data.push({
title: 'recently_used',
data: keyboardState.recentlyUsed,
});
}
return data;
}, [disabledCategory, keyboardState.recentlyUsed]);
return (
<View
style={[
@ -57,10 +71,8 @@ export const EmojiStaticKeyboard = () => {
]}
>
<Animated.FlatList
data={emojisByGroup.filter((category) => {
const title = category.title as CategoryTypes;
return !disabledCategory.includes(title);
})}
extraData={keyboardState.recentlyUsed.length}
data={renderList}
keyExtractor={(item: EmojisByCategory) => item.title}
renderItem={renderItem}
removeClippedSubviews={true}

View File

@ -8,6 +8,7 @@ import Smile from '../assets/Smile';
import Trees from '../assets/Trees';
import Ban from '../assets/Ban';
import Users from '../assets/Users';
import Clock from '../assets/Clock';
export const Icon = ({
iconName,
@ -40,6 +41,8 @@ export const Icon = ({
return <Ban fill={color()} />;
case 'Users':
return <Users fill={color()} />;
case 'Clock':
return <Clock fill={color()} />;
default:
return null;
}

View File

@ -13,7 +13,7 @@ export class SingleEmoji extends React.Component<{
render() {
const { item, emojiSize, onPress } = this.props;
return (
<TouchableOpacity onPress={() => onPress()} style={styles.container}>
<TouchableOpacity onPress={onPress} style={styles.container}>
<View style={styles.iconContainer}>
<Text style={{ fontSize: emojiSize }}>{item.emoji}</Text>
</View>

View File

@ -37,6 +37,7 @@ export type KeyboardProps = {
}) => void;
translation?: CategoryTranslation;
disabledCategory?: CategoryTypes[];
enableRecentlyUsed?: boolean;
categoryPosition?: CategoryPosition;
};
export type ContextValues = {

View File

@ -36,6 +36,7 @@ export const defaultKeyboardContext: Required<KeyboardProps> = {
},
translation: en,
disabledCategory: [],
enableRecentlyUsed: false,
categoryPosition: 'floating',
};

View File

@ -1,12 +1,12 @@
import type { EmojiType } from 'src/types';
import type { JsonEmoji } from 'src/types';
export type RecentEmojiState = {
recentlyUsed: EmojiType[];
recentlyUsed: JsonEmoji[];
};
export type RecentEmojiAction =
| { type: 'RECENT_EMOJI_ADD'; payload: EmojiType }
| { type: 'RECENT_EMOJI_REMOVE'; payload: EmojiType }
| { type: 'RECENT_EMOJI_ADD'; payload: JsonEmoji }
| { type: 'RECENT_EMOJI_REMOVE'; payload: JsonEmoji }
| { type: 'RECENT_EMOJI_CLEAR' };
export default function recentEmojiReducer(
@ -17,7 +17,7 @@ export default function recentEmojiReducer(
case 'RECENT_EMOJI_ADD':
return {
...state,
recentlyUsed: [...filterEmoji(state, action.payload), action.payload],
recentlyUsed: [action.payload, ...filterEmoji(state, action.payload)],
};
case 'RECENT_EMOJI_REMOVE':
return {
@ -31,5 +31,5 @@ export default function recentEmojiReducer(
}
}
const filterEmoji = (state: RecentEmojiState, emoji: EmojiType) =>
const filterEmoji = (state: RecentEmojiState, emoji: JsonEmoji) =>
state.recentlyUsed.filter((usedEmoji) => usedEmoji.emoji !== emoji.emoji);

View File

@ -1,6 +1,7 @@
import type { CategoryTranslation } from '../types';
export const en: CategoryTranslation = {
recently_used: 'Recently used',
smileys_emotion: 'Smileys & Emotion',
people_body: 'People & Body',
animals_nature: 'Animals & Nature',

View File

@ -1,7 +1,8 @@
import type { CategoryTranslation } from '../types';
const pl: CategoryTranslation = {
smileys_emotion: 'Buźki i Emocje',
recently_used: 'Ostatnio używane',
smileys_emotion: 'Buźki i emocje',
people_body: 'Ludzie',
animals_nature: 'Zwierzęta i przyroda',
food_drink: 'Jedzenie i napoje',

View File

@ -19,7 +19,8 @@ export type CategoryTypes =
| 'activities'
| 'objects'
| 'symbols'
| 'flags';
| 'flags'
| 'recently_used';
export type CategoryPosition = 'floating' | 'top' | 'bottom';
@ -33,6 +34,7 @@ export const CATEGORIES: CategoryTypes[] = [
'objects',
'symbols',
'flags',
'recently_used',
];
export type CategoryNavigationItem = {
@ -54,6 +56,7 @@ export const CATEGORIES_NAVIGATION: CategoryNavigationItem[] = [
{ icon: 'Lightbulb', category: 'objects' },
{ icon: 'Ban', category: 'symbols' },
{ icon: 'Flag', category: 'flags' },
{ icon: 'Clock', category: 'recently_used' },
];
export type EmojisByCategory = {