Migrate EmojiPicker to hooks

This commit is contained in:
Danish Ahmed Mirza 2022-06-24 01:22:53 +05:30 committed by Danish
parent cbc6892084
commit 597a6836e6
3 changed files with 122 additions and 192 deletions

View File

@ -26,50 +26,42 @@ const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
); );
}; };
class EmojiCategory extends React.Component<IEmojiCategory> { const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...props }: IEmojiCategory) => {
renderItem(emoji: IEmoji) { const renderItem = (emoji: IEmoji) => (
const { baseUrl, onEmojiSelected } = this.props; <TouchableOpacity
return ( activeOpacity={0.7}
<TouchableOpacity // @ts-ignore
activeOpacity={0.7} key={emoji && emoji.isCustom ? emoji.content : emoji}
// @ts-ignore onPress={() => onEmojiSelected(emoji)}
key={emoji && emoji.isCustom ? emoji.content : emoji} testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
onPress={() => onEmojiSelected(emoji)} {renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`} </TouchableOpacity>
> );
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity> if (!width) {
); return null;
} }
render() { const numColumns = Math.trunc(width / EMOJI_SIZE);
const { emojis, width } = this.props; const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
if (!width) { return (
return null; <FlatList
} contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
const numColumns = Math.trunc(width / EMOJI_SIZE); key={`emoji-category-${width}`}
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2; // @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
return ( data={emojis}
<FlatList extraData={{ baseUrl, onEmojiSelected, width, ...props }}
contentContainerStyle={{ marginHorizontal }} renderItem={({ item }) => renderItem(item)}
// rerender FlatList in case of width changes numColumns={numColumns}
key={`emoji-category-${width}`} initialNumToRender={45}
// @ts-ignore removeClippedSubviews
keyExtractor={item => (item && item.isCustom && item.content) || item} {...scrollPersistTaps}
data={emojis} keyboardDismissMode={'none'}
extraData={this.props} />
renderItem={({ item }) => this.renderItem(item)} );
numColumns={numColumns} });
initialNumToRender={45}
removeClippedSubviews
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
}
}
export default EmojiCategory; export default EmojiCategory;

View File

@ -2,55 +2,41 @@ import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native'; import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import { useTheme } from '../../theme';
import { TSupportedThemes } from '../../theme';
interface ITabBarProps { interface ITabBarProps {
goToPage?: (page: number) => void; goToPage?: (page: number) => void;
activeTab?: number; activeTab?: number;
tabs?: string[]; tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>; tabEmojiStyle: StyleProp<TextStyle>;
theme: TSupportedThemes;
} }
export default class TabBar extends React.Component<ITabBarProps> { const TabBar = React.memo(({ activeTab, tabs, goToPage, tabEmojiStyle }: ITabBarProps) => {
shouldComponentUpdate(nextProps: ITabBarProps) { const { colors } = useTheme();
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() { return (
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props; <View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: colors.tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
});
return ( export default TabBar;
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -1,11 +1,8 @@
import React, { Component } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleProp, TextStyle, View } from 'react-native'; import { StyleProp, TextStyle, View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from 'react-native-fast-image';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
@ -16,74 +13,43 @@ import { emojisByCategory } from './emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode'; import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../lib/methods/helpers/log'; import log from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants'; import { useTheme } from '../../theme';
import { TSupportedThemes } from '../../theme'; import { IEmoji, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions'; import { useAppSelector } from '../../lib/hooks';
interface IEmojiPickerProps { interface IEmojiPickerProps {
isMessageContainsOnlyEmoji?: boolean;
getCustomEmoji?: TGetCustomEmoji;
baseUrl: string;
customEmojis: ICustomEmojis;
style?: StyleProp<ImageStyle>;
theme: TSupportedThemes;
onEmojiSelected: (emoji: string, shortname?: string) => void; onEmojiSelected: (emoji: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>; tabEmojiStyle?: StyleProp<TextStyle>;
} }
interface IEmojiPickerState { const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPickerProps) => {
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[]; const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]);
customEmojis: any; const [show, setShow] = useState(false);
show: boolean; const [width, setWidth] = useState(null);
width: number | null; const { colors } = useTheme();
}
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> { const allCustomEmojis: ICustomEmojis = useAppSelector(state => state.customEmojis);
constructor(props: IEmojiPickerProps) { const baseUrl = useAppSelector(state=>state.server?.server)
super(props); const customEmojis = Object.keys(allCustomEmojis)
const customEmojis = Object.keys(props.customEmojis) .filter(item => item === allCustomEmojis[item].name)
.filter(item => item === props.customEmojis[item].name) .map(item => ({
.map(item => ({ content: allCustomEmojis[item].name,
content: props.customEmojis[item].name, extension: allCustomEmojis[item].extension,
extension: props.customEmojis[item].extension, isCustom: true
isCustom: true }));
}));
this.state = { useEffect(() => {
frequentlyUsed: [], const init = async () => {
customEmojis, await updateFrequentlyUsed();
show: false, setShow(true);
width: null
}; };
} init();
}, []);
async componentDidMount() { const handleEmojiSelect = (emoji: IEmoji) => {
await this.updateFrequentlyUsed();
this.setState({ show: true });
}
shouldComponentUpdate(nextProps: IEmojiPickerProps, nextState: IEmojiPickerState) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextState.show !== show) {
return true;
}
if (nextState.width !== width) {
return true;
}
if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
return false;
}
onEmojiSelected = (emoji: IEmoji) => {
try { try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) { if (emoji.isCustom) {
this._addFrequentlyUsed({ _addFrequentlyUsed({
content: emoji.content, content: emoji.content,
extension: emoji.extension, extension: emoji.extension,
isCustom: true isCustom: true
@ -91,7 +57,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
onEmojiSelected(`:${emoji.content}:`); onEmojiSelected(`:${emoji.content}:`);
} else { } else {
const content = emoji; const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false }); _addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`; const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname); onEmojiSelected(shortnameToUnicode(shortname), shortname);
} }
@ -100,7 +66,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} }
}; };
_addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => { const _addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel; let freqEmojiRecord: TFrequentlyUsedEmojiModel;
@ -127,29 +93,26 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
}); });
}); });
updateFrequentlyUsed = async () => { const updateFrequentlyUsed = async () => {
const db = database.active; const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']); const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => { const frequentlyUsedEmojis = frequentlyUsedOrdered.map(item => {
if (item.isCustom) { if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom }; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }
return shortnameToUnicode(`${item.content}`); return shortnameToUnicode(`${item.content}`);
}); }) as IEmoji[];
this.setState({ frequentlyUsed }); setFrequentlyUsed(frequentlyUsedEmojis);
}; };
onLayout = ({ const onLayout = ({
nativeEvent: { nativeEvent: {
layout: { width } layout: { width }
} }
}: any) => this.setState({ width }); }: any) => setWidth(width);
renderCategory(category: keyof typeof emojisByCategory, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props;
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
emojis = frequentlyUsed; emojis = frequentlyUsed;
@ -160,47 +123,36 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} }
return ( return (
<EmojiCategory <EmojiCategory
emojis={emojis} emojis={emojis as IEmoji[]}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)}
style={styles.categoryContainer} style={styles.categoryContainer}
width={width} width={width}
baseUrl={baseUrl} baseUrl={baseUrl}
tabLabel={label} tabLabel={label}
/> />
); );
};
if (!show) {
return null;
} }
return (
render() { <View onLayout={onLayout} style={{ flex: 1 }}>
const { show, frequentlyUsed } = this.state; <ScrollableTabView
const { tabEmojiStyle, theme } = this.props; renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} />}
contentProps={{
if (!show) { keyboardShouldPersistTaps: 'always',
return null; keyboardDismissMode: 'none'
} }}
return ( style={{ backgroundColor: colors.focusedBackground }}>
<View onLayout={this.onLayout} style={{ flex: 1 }}> {categories.tabs.map((tab: any, i) =>
<ScrollableTabView i === 0 && frequentlyUsed.length === 0
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} ? null // when no frequentlyUsed don't show the tab
contentProps={{ : renderCategory(tab.category, i, tab.tabLabel)
keyboardShouldPersistTaps: 'always', )}
keyboardDismissMode: 'none' </ScrollableTabView>
}} </View>
style={{ backgroundColor: themes[theme].focusedBackground }} );
>
{categories.tabs.map((tab: any, i) =>
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
</ScrollableTabView>
</View>
);
}
}
const mapStateToProps = (state: IApplicationState) => ({
customEmojis: state.customEmojis,
baseUrl: state.share.server.server || state.server.server
}); });
export default connect(mapStateToProps)(EmojiPicker); export default EmojiPicker;