Add searchbar for Emojis

This commit is contained in:
Danish Ahmed Mirza 2022-06-28 01:02:34 +05:30 committed by Danish
parent 349c56ba45
commit 8ea5755345
8 changed files with 232 additions and 29 deletions

View File

@ -47,7 +47,7 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, tab
return ( return (
<FlatList <FlatList
// rerender FlatList in case of width changes // rerender FlatList in case of width changes
key={`emoji-category-${numColumns}`} key={`emoji-category-${width}`}
// @ts-ignore // @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item} keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis} data={emojis}

View File

@ -7,11 +7,11 @@ import { CustomIcon } from '../CustomIcon';
import styles from './styles'; import styles from './styles';
import { IFooterProps } from './interfaces'; import { IFooterProps } from './interfaces';
const Footer = React.memo(({ onBackspacePressed }: IFooterProps) => { const Footer = React.memo(({ onSearchPressed, onBackspacePressed }: IFooterProps) => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<View style={[styles.footerContainer, { backgroundColor: colors.bannerBackground }]}> <View style={[styles.footerContainer, { backgroundColor: colors.bannerBackground }]}>
<BorderlessButton activeOpacity={0.7} onPress={() => console.log('Search!')} style={styles.footerButtonsContainer}> <BorderlessButton activeOpacity={0.7} onPress={onSearchPressed} style={styles.footerButtonsContainer}>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' /> <CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
</BorderlessButton> </BorderlessButton>

View File

@ -56,7 +56,7 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
const content = emoji; const content = emoji;
_addFrequentlyUsed({ content, isCustom: false }); _addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`; const shortname = `:${emoji}:`;
onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname); onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname));
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -131,8 +131,6 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
); );
}; };
const onBackspacePressed = () => onItemClicked(EventTypes.BACKSPACE_PRESSED);
if (!show) { if (!show) {
return null; return null;
} }
@ -154,7 +152,10 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
: renderCategory(tab.category, i, tab.tabLabel, tabsCount) : renderCategory(tab.category, i, tab.tabLabel, tabsCount)
)} )}
</ScrollableTabView> </ScrollableTabView>
<Footer onBackspacePressed={onBackspacePressed} /> <Footer
onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
/>
</View> </View>
); );
}); });

View File

@ -2,16 +2,19 @@ import { StyleProp, TextStyle } from 'react-native';
export enum EventTypes { export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed', EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed' BACKSPACE_PRESSED = 'backspacePressed',
SEARCH_PRESSED = 'searchPressed'
} }
export interface IEmojiPickerProps { export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: string, shortname?: string) => void; baseUrl: string;
onItemClicked: (event: EventTypes, emoji?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>; tabEmojiStyle?: StyleProp<TextStyle>;
} }
export interface IFooterProps { export interface IFooterProps {
onBackspacePressed: () => void; onBackspacePressed: () => void;
onSearchPressed: () => void;
} }
export interface ITabBarProps { export interface ITabBarProps {

View File

@ -0,0 +1,117 @@
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, TextInput, FlatList } from 'react-native';
import { orderBy } from 'lodash';
import FormTextInput from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
interface IEmojiSearchbarProps {
openEmoji: () => void;
onChangeText: (value: string) => void;
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
baseUrl: string;
}
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji.name) {
return <CustomEmoji style={{ height: size, width: size }} emoji={emoji} baseUrl={baseUrl} />;
}
return <Text style={{ fontSize: size }}>{shortnameToUnicode(`:${emoji}:`)}</Text>;
};
const EmojiSearchbar = React.forwardRef<TextInput, IEmojiSearchbarProps>(
({ openEmoji, onChangeText, emojis, onEmojiSelected, baseUrl }, ref) => {
console.log('Emojis', emojis);
const { colors, theme } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const [frequentlyUsed, setFrequentlyUsed] = useState([]);
const getFrequentlyUsedEmojis = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsedEmojis = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { name: item.content, extension: item.extension };
}
return item.content;
});
// @ts-ignore
setFrequentlyUsed(frequentlyUsedEmojis);
};
useEffect(() => {
getFrequentlyUsedEmojis();
}, []);
const handleTextChange = (text: string) => {
setSearchText(text);
onChangeText(text);
if (!text) getFrequentlyUsedEmojis();
};
const renderItem = (emoji: IEmoji) => {
const emojiSize = 30;
return (
<View style={{ justifyContent: 'center', marginHorizontal: 2 }}>
<TouchableOpacity
activeOpacity={0.7}
// @ts-ignore
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}>
{renderEmoji(emoji, emojiSize, baseUrl)}
</TouchableOpacity>
</View>
);
};
return (
<View>
<View style={{ height: 50, paddingHorizontal: 5 }}>
<FlatList horizontal data={searchText ? emojis : frequentlyUsed} renderItem={({ item }) => renderItem(item)} />
</View>
<View
style={{
flexDirection: 'row',
height: 50,
marginVertical: 10,
justifyContent: 'center',
alignItems: 'center'
}}>
<TouchableOpacity
style={{ marginHorizontal: 10, justifyContent: 'center', height: '100%' }}
activeOpacity={0.7}
onPress={openEmoji}>
<CustomIcon name='chevron-left' size={30} color={colors.collapsibleChevron} />
</TouchableOpacity>
<View style={{ flex: 1 }}>
<FormTextInput
inputRef={ref}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
returnKeyType='search'
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
style={{ backgroundColor: colors.passcodeButtonActive, padding: 10, borderRadius: 5 }}
containerStyle={{ height: '100%', justifyContent: 'center', marginBottom: 0, marginRight: 15 }}
value={searchText}
theme={theme}
onClearInput={() => handleTextChange('')}
iconRight={'search'}
/>
</View>
</View>
</View>
);
}
);
export default EmojiSearchbar;

View File

@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#'; export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_TRACKING_TYPE_CANNED = '!'; export const MENTIONS_TRACKING_TYPE_CANNED = '!';
export const MENTIONS_COUNT_TO_DISPLAY = 4; export const MENTIONS_COUNT_TO_DISPLAY = 4;
export const MAX_EMOJIS_TO_DISPLAY = 20;
export const TIMEOUT_CLOSE_EMOJI = 300; export const TIMEOUT_CLOSE_EMOJI = 300;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native'; import { Alert, Keyboard, NativeModules, Text, View, TextInput as RNTextInput } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard'; import { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker'; import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
@ -34,7 +34,8 @@ import {
MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_ROOMS, MENTIONS_TRACKING_TYPE_ROOMS,
MENTIONS_TRACKING_TYPE_USERS, MENTIONS_TRACKING_TYPE_USERS,
TIMEOUT_CLOSE_EMOJI TIMEOUT_CLOSE_EMOJI,
MAX_EMOJIS_TO_DISPLAY
} from './constants'; } from './constants';
import CommandsPreview from './CommandsPreview'; import CommandsPreview from './CommandsPreview';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
@ -60,6 +61,8 @@ import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types'; import { ChatsStackParamList } from '../../stacks/types';
import { EventTypes } from '../EmojiPicker/interfaces'; import { EventTypes } from '../EmojiPicker/interfaces';
import EmojiSearchbar from './EmojiSearchbar';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -130,6 +133,8 @@ interface IMessageBoxState {
tshow: boolean; tshow: boolean;
mentionLoading: boolean; mentionLoading: boolean;
permissionToUpload: boolean; permissionToUpload: boolean;
showEmojiSearchbar: boolean;
searchedEmojis: any[];
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -161,6 +166,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
private typingTimeout: any; private typingTimeout: any;
private emojiSearchbarRef: any;
static defaultProps = { static defaultProps = {
message: { message: {
id: '' id: ''
@ -184,11 +191,14 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
command: {}, command: {},
tshow: this.sendThreadToChannel, tshow: this.sendThreadToChannel,
mentionLoading: false, mentionLoading: false,
permissionToUpload: true permissionToUpload: true,
showEmojiSearchbar: false,
searchedEmojis: []
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
this.focused = false; this.focused = false;
this.emojiSearchbarRef = React.createRef<RNTextInput>();
const libPickerLabels = { const libPickerLabels = {
cropperChooseText: I18n.t('Choose'), cropperChooseText: I18n.t('Choose'),
@ -327,7 +337,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow, tshow,
mentionLoading, mentionLoading,
trackingType, trackingType,
permissionToUpload permissionToUpload,
showEmojiSearchbar,
searchedEmojis
} = this.state; } = this.state;
const { const {
@ -395,6 +407,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextProps.goToCannedResponses !== goToCannedResponses) { if (nextProps.goToCannedResponses !== goToCannedResponses) {
return true; return true;
} }
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!dequal(nextState.searchedEmojis, searchedEmojis)) {
return true;
}
return false; return false;
} }
@ -578,28 +596,40 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
onItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: string }) => { onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: string }) => {
const { eventType, emoji } = params; const { eventType, emoji } = params;
const { text } = this; const { text } = this;
let newText = ''; let newText = '';
// if messagebox has an active cursor // if messagebox has an active cursor
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
if (eventType === EventTypes.BACKSPACE_PRESSED) { let newCursor;
let charsToRemove = 1;
if (cursor > 1) { switch (eventType) {
case EventTypes.BACKSPACE_PRESSED:
let charsToRemove = 1;
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/; const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
const lastEmoji = text.substr(cursor - 2, cursor); const lastEmoji = text.substr(text.length - 2, text.length);
// Check if last character is an emoji
if (emojiRegex.test(lastEmoji)) charsToRemove = 2; if (emojiRegex.test(lastEmoji)) charsToRemove = 2;
} newText = text.substr(0, text.length - charsToRemove);
newText = text.substr(0, cursor - charsToRemove); newCursor = cursor - charsToRemove;
this.setInput(newText, { start: cursor - charsToRemove, end: cursor - charsToRemove }); this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(newText !== ''); this.setShowSend(newText !== '');
} else if (eventType === EventTypes.EMOJI_PRESSED) { break;
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`; case EventTypes.EMOJI_PRESSED:
const newCursor = cursor + emoji.length; newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
this.setInput(newText, { start: newCursor, end: newCursor }); newCursor = cursor + emoji.length;
this.setShowSend(true); this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
break;
case EventTypes.SEARCH_PRESSED:
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true }, () => {
this.emojiSearchbarRef.current.focus();
});
break;
default:
// Do nothing
} }
}; };
@ -893,7 +923,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openEmoji = () => { openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI); logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true }); this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
}; };
recordingCallback = (recording: any) => { recordingCallback = (recording: any) => {
@ -1095,6 +1125,55 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
); );
}; };
searchEmojis = debounce(async (keyword: any) => {
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
const likeString = sanitizeLikeString(keyword);
const whereClause = [];
if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
}
let customEmojis = await customEmojisCollection.query(...whereClause).fetch();
customEmojis = customEmojis.slice(0, MAX_EMOJIS_TO_DISPLAY / 2);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MAX_EMOJIS_TO_DISPLAY / 2);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MAX_EMOJIS_TO_DISPLAY);
this.setState({ searchedEmojis: mergedEmojis });
}, 300);
renderEmojiSearchbar = () => {
const { showEmojiSearchbar, searchedEmojis } = this.state;
const { baseUrl } = this.props;
const onChangeText = (value: string) => {
this.searchEmojis(value);
};
const onEmojiSelected = (emoji: any) => {
let selectedEmoji;
if (emoji.name) {
selectedEmoji = `:${emoji.name}:`;
} else {
selectedEmoji = shortnameToUnicode(`:${emoji}:`);
}
const { text } = this;
let newText = '';
const { start, end } = this.selection;
const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${selectedEmoji}${text.substr(cursor)}`;
const newCursor = cursor + selectedEmoji.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
};
return showEmojiSearchbar ? (
<EmojiSearchbar
ref={this.emojiSearchbarRef}
openEmoji={this.openEmoji}
onChangeText={onChangeText}
emojis={searchedEmojis}
baseUrl={baseUrl}
onEmojiSelected={onEmojiSelected}
/>
) : null;
};
renderContent = () => { renderContent = () => {
const { const {
recording, recording,
@ -1209,6 +1288,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
{recordAudio} {recordAudio}
</View> </View>
{this.renderSendToChannel()} {this.renderSendToChannel()}
{this.renderEmojiSearchbar()}
</View> </View>
{children} {children}
</> </>
@ -1236,7 +1316,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null} kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }} kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned} onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onItemSelected} onItemSelected={this.onKeyboardItemSelected}
trackInteractive trackInteractive
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
addBottomView addBottomView

View File

@ -482,6 +482,7 @@
"Search_Messages": "Search Messages", "Search_Messages": "Search Messages",
"Search": "Search", "Search": "Search",
"Search_by": "Search by", "Search_by": "Search by",
"Search_emoji": "Search emoji",
"Search_global_users": "Search for global users", "Search_global_users": "Search for global users",
"Search_global_users_description": "If you turn-on, you can search for any user from others companies or servers.", "Search_global_users_description": "If you turn-on, you can search for any user from others companies or servers.",
"Seconds": "{{second}} seconds", "Seconds": "{{second}} seconds",