Add searchbar for Emojis
This commit is contained in:
parent
349c56ba45
commit
8ea5755345
|
@ -47,7 +47,7 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, tab
|
|||
return (
|
||||
<FlatList
|
||||
// rerender FlatList in case of width changes
|
||||
key={`emoji-category-${numColumns}`}
|
||||
key={`emoji-category-${width}`}
|
||||
// @ts-ignore
|
||||
keyExtractor={item => (item && item.isCustom && item.content) || item}
|
||||
data={emojis}
|
||||
|
|
|
@ -7,11 +7,11 @@ import { CustomIcon } from '../CustomIcon';
|
|||
import styles from './styles';
|
||||
import { IFooterProps } from './interfaces';
|
||||
|
||||
const Footer = React.memo(({ onBackspacePressed }: IFooterProps) => {
|
||||
const Footer = React.memo(({ onSearchPressed, onBackspacePressed }: IFooterProps) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<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' />
|
||||
</BorderlessButton>
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
|
|||
const content = emoji;
|
||||
_addFrequentlyUsed({ content, isCustom: false });
|
||||
const shortname = `:${emoji}:`;
|
||||
onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname);
|
||||
onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname));
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
|
@ -131,8 +131,6 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
|
|||
);
|
||||
};
|
||||
|
||||
const onBackspacePressed = () => onItemClicked(EventTypes.BACKSPACE_PRESSED);
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
@ -154,7 +152,10 @@ const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerPr
|
|||
: renderCategory(tab.category, i, tab.tabLabel, tabsCount)
|
||||
)}
|
||||
</ScrollableTabView>
|
||||
<Footer onBackspacePressed={onBackspacePressed} />
|
||||
<Footer
|
||||
onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
|
||||
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,16 +2,19 @@ import { StyleProp, TextStyle } from 'react-native';
|
|||
|
||||
export enum EventTypes {
|
||||
EMOJI_PRESSED = 'emojiPressed',
|
||||
BACKSPACE_PRESSED = 'backspacePressed'
|
||||
BACKSPACE_PRESSED = 'backspacePressed',
|
||||
SEARCH_PRESSED = 'searchPressed'
|
||||
}
|
||||
|
||||
export interface IEmojiPickerProps {
|
||||
onItemClicked: (event: EventTypes, emoji?: string, shortname?: string) => void;
|
||||
baseUrl: string;
|
||||
onItemClicked: (event: EventTypes, emoji?: string) => void;
|
||||
tabEmojiStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export interface IFooterProps {
|
||||
onBackspacePressed: () => void;
|
||||
onSearchPressed: () => void;
|
||||
}
|
||||
|
||||
export interface ITabBarProps {
|
||||
|
|
|
@ -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;
|
|
@ -4,5 +4,6 @@ export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
|
|||
export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
|
||||
export const MENTIONS_TRACKING_TYPE_CANNED = '!';
|
||||
export const MENTIONS_COUNT_TO_DISPLAY = 4;
|
||||
export const MAX_EMOJIS_TO_DISPLAY = 20;
|
||||
|
||||
export const TIMEOUT_CLOSE_EMOJI = 300;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { KeyboardAccessoryView } from 'react-native-ui-lib/keyboard';
|
||||
import ImagePicker, { Image, ImageOrVideo, Options } from 'react-native-image-crop-picker';
|
||||
|
@ -34,7 +34,8 @@ import {
|
|||
MENTIONS_TRACKING_TYPE_EMOJIS,
|
||||
MENTIONS_TRACKING_TYPE_ROOMS,
|
||||
MENTIONS_TRACKING_TYPE_USERS,
|
||||
TIMEOUT_CLOSE_EMOJI
|
||||
TIMEOUT_CLOSE_EMOJI,
|
||||
MAX_EMOJIS_TO_DISPLAY
|
||||
} from './constants';
|
||||
import CommandsPreview from './CommandsPreview';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
|
@ -60,6 +61,8 @@ import { Services } from '../../lib/services';
|
|||
import { TSupportedThemes } from '../../theme';
|
||||
import { ChatsStackParamList } from '../../stacks/types';
|
||||
import { EventTypes } from '../EmojiPicker/interfaces';
|
||||
import EmojiSearchbar from './EmojiSearchbar';
|
||||
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
|
||||
|
||||
require('./EmojiKeyboard');
|
||||
|
||||
|
@ -130,6 +133,8 @@ interface IMessageBoxState {
|
|||
tshow: boolean;
|
||||
mentionLoading: boolean;
|
||||
permissionToUpload: boolean;
|
||||
showEmojiSearchbar: boolean;
|
||||
searchedEmojis: any[];
|
||||
}
|
||||
|
||||
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
||||
|
@ -161,6 +166,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
|
||||
private typingTimeout: any;
|
||||
|
||||
private emojiSearchbarRef: any;
|
||||
|
||||
static defaultProps = {
|
||||
message: {
|
||||
id: ''
|
||||
|
@ -184,11 +191,14 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
command: {},
|
||||
tshow: this.sendThreadToChannel,
|
||||
mentionLoading: false,
|
||||
permissionToUpload: true
|
||||
permissionToUpload: true,
|
||||
showEmojiSearchbar: false,
|
||||
searchedEmojis: []
|
||||
};
|
||||
this.text = '';
|
||||
this.selection = { start: 0, end: 0 };
|
||||
this.focused = false;
|
||||
this.emojiSearchbarRef = React.createRef<RNTextInput>();
|
||||
|
||||
const libPickerLabels = {
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
|
@ -327,7 +337,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
tshow,
|
||||
mentionLoading,
|
||||
trackingType,
|
||||
permissionToUpload
|
||||
permissionToUpload,
|
||||
showEmojiSearchbar,
|
||||
searchedEmojis
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
|
@ -395,6 +407,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
if (nextProps.goToCannedResponses !== goToCannedResponses) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
|
||||
return true;
|
||||
}
|
||||
if (!dequal(nextState.searchedEmojis, searchedEmojis)) {
|
||||
return true;
|
||||
}
|
||||
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 { text } = this;
|
||||
let newText = '';
|
||||
// if messagebox has an active cursor
|
||||
const { start, end } = this.selection;
|
||||
const cursor = Math.max(start, end);
|
||||
if (eventType === EventTypes.BACKSPACE_PRESSED) {
|
||||
let charsToRemove = 1;
|
||||
if (cursor > 1) {
|
||||
let newCursor;
|
||||
|
||||
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 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;
|
||||
}
|
||||
newText = text.substr(0, cursor - charsToRemove);
|
||||
this.setInput(newText, { start: cursor - charsToRemove, end: cursor - charsToRemove });
|
||||
this.setShowSend(newText !== '');
|
||||
} else if (eventType === EventTypes.EMOJI_PRESSED) {
|
||||
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
|
||||
const newCursor = cursor + emoji.length;
|
||||
this.setInput(newText, { start: newCursor, end: newCursor });
|
||||
this.setShowSend(true);
|
||||
newText = text.substr(0, text.length - charsToRemove);
|
||||
newCursor = cursor - charsToRemove;
|
||||
this.setInput(newText, { start: newCursor, end: newCursor });
|
||||
this.setShowSend(newText !== '');
|
||||
break;
|
||||
case EventTypes.EMOJI_PRESSED:
|
||||
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
|
||||
newCursor = cursor + emoji.length;
|
||||
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 = () => {
|
||||
logEvent(events.ROOM_OPEN_EMOJI);
|
||||
this.setState({ showEmojiKeyboard: true });
|
||||
this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
const {
|
||||
recording,
|
||||
|
@ -1209,6 +1288,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
{recordAudio}
|
||||
</View>
|
||||
{this.renderSendToChannel()}
|
||||
{this.renderEmojiSearchbar()}
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
|
@ -1236,7 +1316,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
|
|||
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
|
||||
kbInitialProps={{ theme }}
|
||||
onKeyboardResigned={this.onKeyboardResigned}
|
||||
onItemSelected={this.onItemSelected}
|
||||
onItemSelected={this.onKeyboardItemSelected}
|
||||
trackInteractive
|
||||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
|
|
|
@ -482,6 +482,7 @@
|
|||
"Search_Messages": "Search Messages",
|
||||
"Search": "Search",
|
||||
"Search_by": "Search by",
|
||||
"Search_emoji": "Search emoji",
|
||||
"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.",
|
||||
"Seconds": "{{second}} seconds",
|
||||
|
|
Loading…
Reference in New Issue