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:
parent
c0cbef73c2
commit
de2d2f3e0c
|
@ -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 |
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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')}
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -37,6 +37,7 @@ export type KeyboardProps = {
|
|||
}) => void;
|
||||
translation?: CategoryTranslation;
|
||||
disabledCategory?: CategoryTypes[];
|
||||
enableRecentlyUsed?: boolean;
|
||||
categoryPosition?: CategoryPosition;
|
||||
};
|
||||
export type ContextValues = {
|
||||
|
|
|
@ -36,6 +36,7 @@ export const defaultKeyboardContext: Required<KeyboardProps> = {
|
|||
},
|
||||
translation: en,
|
||||
disabledCategory: [],
|
||||
enableRecentlyUsed: false,
|
||||
categoryPosition: 'floating',
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue