Migrate EmojiPicker to hooks
This commit is contained in:
parent
cbc6892084
commit
597a6836e6
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue