diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx
index 94065dd84..e520d482c 100644
--- a/app/containers/ActionSheet/ActionSheet.tsx
+++ b/app/containers/ActionSheet/ActionSheet.tsx
@@ -140,8 +140,8 @@ const ActionSheet = React.memo(
style={{ ...styles.container, ...bottomSheet }}
backgroundStyle={{ backgroundColor: colors.focusedBackground }}
onChange={index => index === -1 && onClose()}
- // We need this to allow horizontal swipe gestures inside bottom sheet like in reaction picker
- enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
+ activeOffsetY={[-1, 1]}
+ failOffsetX={[-5, 5]}
{...androidTablet}
>
diff --git a/app/containers/EmojiPicker/EmojiCategory.tsx b/app/containers/EmojiPicker/EmojiCategory.tsx
index fc8188982..0b00239a3 100644
--- a/app/containers/EmojiPicker/EmojiCategory.tsx
+++ b/app/containers/EmojiPicker/EmojiCategory.tsx
@@ -1,5 +1,7 @@
import React from 'react';
-import { FlatList, Text, TouchableOpacity } from 'react-native';
+import { Text, TouchableOpacity } from 'react-native';
+import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
+import { FlatList as GHFlatList } from 'react-native-gesture-handler';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
@@ -10,7 +12,7 @@ import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const MAX_EMOJI_SIZE = 50;
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
- if (emoji && emoji.isCustom) {
+ if (emoji?.isCustom || emoji?.name) {
return (
{
);
};
-const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, tabsCount, ...props }: IEmojiCategory) => {
- const emojiSize = width ? Math.min(width / tabsCount, MAX_EMOJI_SIZE) : MAX_EMOJI_SIZE;
- const numColumns = Math.trunc(width ? width / emojiSize : tabsCount);
- const renderItem = (emoji: IEmoji) => (
- onEmojiSelected(emoji)}
- testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
- {renderEmoji(emoji, emojiSize, baseUrl)}
-
- );
+const EmojiCategory = React.memo(
+ ({ baseUrl, onEmojiSelected, emojis, width, tabsCount, isBottomSheet, ...props }: IEmojiCategory) => {
+ const emojiSize = width ? Math.min(width / tabsCount, MAX_EMOJI_SIZE) : MAX_EMOJI_SIZE;
+ const numColumns = Math.trunc(width ? width / emojiSize : tabsCount);
- if (!width) {
- return null;
+ const FlatList = isBottomSheet ? BottomSheetFlatList : GHFlatList;
+
+ const renderItem = (emoji: IEmoji) => (
+ onEmojiSelected(emoji)}
+ testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}
+ >
+ {renderEmoji(emoji, emojiSize, baseUrl)}
+
+ );
+
+ if (!width) {
+ return null;
+ }
+
+ return (
+ (item && item.isCustom && item.content) || item}
+ data={emojis}
+ extraData={{ baseUrl, onEmojiSelected, width, ...props }}
+ renderItem={({ item }) => renderItem(item)}
+ numColumns={numColumns}
+ initialNumToRender={45}
+ removeClippedSubviews
+ {...scrollPersistTaps}
+ keyboardDismissMode={'none'}
+ />
+ );
}
-
- return (
- (item && item.isCustom && item.content) || item}
- data={emojis}
- extraData={{ baseUrl, onEmojiSelected, width, ...props }}
- renderItem={({ item }) => renderItem(item)}
- numColumns={numColumns}
- initialNumToRender={45}
- removeClippedSubviews
- {...scrollPersistTaps}
- keyboardDismissMode={'none'}
- />
- );
-});
+);
export default EmojiCategory;
diff --git a/app/containers/EmojiPicker/TabBar.tsx b/app/containers/EmojiPicker/TabBar.tsx
index 51945015b..b80385eaa 100644
--- a/app/containers/EmojiPicker/TabBar.tsx
+++ b/app/containers/EmojiPicker/TabBar.tsx
@@ -14,19 +14,11 @@ const TabBar = React.memo(({ activeTab, tabs, goToPage, tabEmojiStyle }: ITabBar
{
- if (goToPage) {
- goToPage(i);
- }
- }}
+ onPress={() => goToPage?.(i)}
style={styles.tab}
testID={`reaction-picker-${tab}`}>
{tab}
- {activeTab === i ? (
-
- ) : (
-
- )}
+
))}
diff --git a/app/containers/EmojiPicker/index.tsx b/app/containers/EmojiPicker/index.tsx
index 5125de0e6..4cc48ba82 100644
--- a/app/containers/EmojiPicker/index.tsx
+++ b/app/containers/EmojiPicker/index.tsx
@@ -19,147 +19,164 @@ import { IEmoji, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definiti
import { useAppSelector } from '../../lib/hooks';
import { IEmojiPickerProps, EventTypes } from './interfaces';
-const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle, isEmojiKeyboard = false }: IEmojiPickerProps) => {
- const [frequentlyUsed, setFrequentlyUsed] = useState([]);
- const [show, setShow] = useState(false);
- const [width, setWidth] = useState(null);
- const { colors } = useTheme();
+const EmojiPicker = React.memo(
+ ({ onItemClicked, tabEmojiStyle, isEmojiKeyboard = false, searching = false, searchedEmojis = [] }: IEmojiPickerProps) => {
+ const [frequentlyUsed, setFrequentlyUsed] = useState([]);
+ const [show, setShow] = useState(false);
+ const [width, setWidth] = useState(null);
+ const { colors } = useTheme();
- const allCustomEmojis: ICustomEmojis = useAppSelector(state => state.customEmojis);
- const baseUrl = useAppSelector(state=>state.server?.server)
- const customEmojis = Object.keys(allCustomEmojis)
- .filter(item => item === allCustomEmojis[item].name)
- .map(item => ({
- content: allCustomEmojis[item].name,
- extension: allCustomEmojis[item].extension,
- isCustom: true
- }));
+ const baseUrl = useAppSelector(state => state.server?.server);
+ const allCustomEmojis: ICustomEmojis = useAppSelector(state => state.customEmojis);
+ const customEmojis = Object.keys(allCustomEmojis)
+ .filter(item => item === allCustomEmojis[item].name)
+ .map(item => ({
+ content: allCustomEmojis[item].name,
+ extension: allCustomEmojis[item].extension,
+ isCustom: true
+ }));
- useEffect(() => {
- const init = async () => {
- await updateFrequentlyUsed();
- setShow(true);
+ useEffect(() => {
+ const init = async () => {
+ await updateFrequentlyUsed();
+ setShow(true);
+ };
+ init();
+ }, []);
+
+ const handleEmojiSelect = (emoji: IEmoji) => {
+ try {
+ if (emoji.isCustom) {
+ _addFrequentlyUsed({
+ content: emoji.content,
+ extension: emoji.extension,
+ isCustom: true
+ });
+ onItemClicked(EventTypes.EMOJI_PRESSED, `:${emoji.content}:`);
+ } else {
+ const content = emoji;
+ _addFrequentlyUsed({ content, isCustom: false });
+ const shortname = `:${emoji}:`;
+ onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname);
+ }
+ } catch (e) {
+ log(e);
+ }
};
- init();
- }, []);
- const handleEmojiSelect = (emoji: IEmoji) => {
- try {
- if (emoji.isCustom) {
- _addFrequentlyUsed({
- content: emoji.content,
- extension: emoji.extension,
- isCustom: true
- });
- onItemClicked(EventTypes.EMOJI_PRESSED, `:${emoji.content}:`);
- } else {
- const content = emoji;
- _addFrequentlyUsed({ content, isCustom: false });
- const shortname = `:${emoji}:`;
- onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname));
+ const _addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
+ const db = database.active;
+ const freqEmojiCollection = db.get('frequently_used_emojis');
+ let freqEmojiRecord: TFrequentlyUsedEmojiModel;
+ try {
+ freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
+ } catch (error) {
+ // Do nothing
}
- } catch (e) {
- log(e);
- }
- };
- const _addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
- const db = database.active;
- const freqEmojiCollection = db.get('frequently_used_emojis');
- let freqEmojiRecord: TFrequentlyUsedEmojiModel;
- try {
- freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
- } catch (error) {
- // Do nothing
- }
-
- await db.write(async () => {
- if (freqEmojiRecord) {
- await freqEmojiRecord.update(f => {
- if (f.count) {
- f.count += 1;
- }
- });
- } else {
- await freqEmojiCollection.create(f => {
- f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
- Object.assign(f, emoji);
- f.count = 1;
- });
- }
+ await db.write(async () => {
+ if (freqEmojiRecord) {
+ await freqEmojiRecord.update(f => {
+ if (f.count) {
+ f.count += 1;
+ }
+ });
+ } else {
+ await freqEmojiCollection.create(f => {
+ f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
+ Object.assign(f, emoji);
+ f.count = 1;
+ });
+ }
+ });
});
- });
- const updateFrequentlyUsed = 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 { content: item.content, extension: item.extension, isCustom: item.isCustom };
+ const updateFrequentlyUsed = 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 { content: item.content, extension: item.extension, isCustom: item.isCustom };
+ }
+ return shortnameToUnicode(`${item.content}`);
+ }) as IEmoji[];
+ setFrequentlyUsed(frequentlyUsedEmojis);
+ };
+
+ const onLayout = ({
+ nativeEvent: {
+ layout: { width }
}
- return shortnameToUnicode(`${item.content}`);
- }) as IEmoji[];
- setFrequentlyUsed(frequentlyUsedEmojis);
- };
+ }: any) => setWidth(width);
- const onLayout = ({
- nativeEvent: {
- layout: { width }
- }
- }: any) => setWidth(width);
-
- const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string, tabsCount: number) => {
- let emojis = [];
- if (i === 0) {
- emojis = frequentlyUsed;
- } else if (i === 1) {
- emojis = customEmojis;
- } else {
- emojis = emojisByCategory[category];
- }
- return (
- handleEmojiSelect(emoji)}
- style={styles.categoryContainer}
- width={width}
- baseUrl={baseUrl}
- tabLabel={label}
- tabsCount={tabsCount}
- />
- );
- };
-
- if (!show) {
- return null;
- }
-
- const tabsCount = frequentlyUsed.length === 0 ? categories.tabs.length - 1 : categories.tabs.length;
-
- return (
-
- }
- contentProps={{
- keyboardShouldPersistTaps: 'always',
- keyboardDismissMode: 'none'
- }}
- style={{ backgroundColor: colors.focusedBackground }}>
- {categories.tabs.map((tab: any, i) =>
- i === 0 && frequentlyUsed.length === 0
- ? null // when no frequentlyUsed don't show the tab
- : renderCategory(tab.category, i, tab.tabLabel, tabsCount)
- )}
-
- {isEmojiKeyboard && (
-
- );
-});
+ );
+ };
+
+ if (!show) {
+ return null;
+ }
+
+ const tabsCount = frequentlyUsed.length === 0 ? categories.tabs.length - 1 : categories.tabs.length;
+
+ return (
+
+ {searching ? (
+ handleEmojiSelect(emoji)}
+ style={styles.categoryContainer}
+ width={width}
+ baseUrl={baseUrl}
+ tabLabel={'searching'}
+ tabsCount={tabsCount}
+ isBottomSheet={!isEmojiKeyboard}
+ />
+ ) : (
+ }
+ contentProps={{
+ keyboardShouldPersistTaps: 'always',
+ keyboardDismissMode: 'none'
+ }}
+ style={{ backgroundColor: colors.focusedBackground }}
+ >
+ {categories.tabs.map((tab: any, i) =>
+ i === 0 && frequentlyUsed.length === 0
+ ? null // when no frequentlyUsed don't show the tab
+ : renderCategory(tab.category, i, tab.tabLabel, tabsCount)
+ )}
+
+ )}
+ {isEmojiKeyboard && (
+
+ );
+ }
+);
export default EmojiPicker;
diff --git a/app/containers/EmojiPicker/interfaces.ts b/app/containers/EmojiPicker/interfaces.ts
index 9e5cc8f0f..75ac29c5c 100644
--- a/app/containers/EmojiPicker/interfaces.ts
+++ b/app/containers/EmojiPicker/interfaces.ts
@@ -1,5 +1,7 @@
import { StyleProp, TextStyle } from 'react-native';
+import { IEmoji } from '../../definitions';
+
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed',
@@ -7,9 +9,11 @@ export enum EventTypes {
}
export interface IEmojiPickerProps {
- onItemClicked: (event: EventTypes, emoji?: string) => void;
+ onItemClicked: (event: EventTypes, emoji?: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp;
isEmojiKeyboard?: boolean;
+ searching?: boolean;
+ searchedEmojis?: (string | IEmoji)[];
}
export interface IFooterProps {
diff --git a/app/containers/MessageBox/EmojiSearchbar.tsx b/app/containers/MessageBox/EmojiSearchbar.tsx
index ad46eaf1a..506020070 100644
--- a/app/containers/MessageBox/EmojiSearchbar.tsx
+++ b/app/containers/MessageBox/EmojiSearchbar.tsx
@@ -2,7 +2,7 @@ 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 { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
@@ -31,7 +31,7 @@ const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
const EmojiSearchbar = React.forwardRef(
({ openEmoji, onChangeText, emojis, onEmojiSelected, baseUrl }, ref) => {
- const { colors, theme } = useTheme();
+ const { colors } = useTheme();
const [searchText, setSearchText] = useState('');
const [frequentlyUsed, setFrequentlyUsed] = useState([]);
@@ -103,7 +103,6 @@ const EmojiSearchbar = React.forwardRef(
style={[styles.emojiSearchbar, { backgroundColor: colors.passcodeButtonActive }]}
containerStyle={styles.textInputContainer}
value={searchText}
- theme={theme}
onClearInput={() => handleTextChange('')}
iconRight={'search'}
/>
diff --git a/app/definitions/IEmoji.ts b/app/definitions/IEmoji.ts
index 0dcf298bf..463bdac6f 100644
--- a/app/definitions/IEmoji.ts
+++ b/app/definitions/IEmoji.ts
@@ -36,6 +36,7 @@ export interface IEmojiCategory {
style: StyleProp;
tabLabel: string;
tabsCount: number;
+ isBottomSheet: boolean;
}
export type TGetCustomEmoji = (name: string) => any;
diff --git a/app/views/RoomView/ReactionPicker.tsx b/app/views/RoomView/ReactionPicker.tsx
index 19c028180..62a62b0e3 100644
--- a/app/views/RoomView/ReactionPicker.tsx
+++ b/app/views/RoomView/ReactionPicker.tsx
@@ -1,87 +1,86 @@
import React from 'react';
import { View } from 'react-native';
-import { connect } from 'react-redux';
-import Modal from 'react-native-modal';
+import { Q } from '@nozbe/watermelondb';
import EmojiPicker from '../../containers/EmojiPicker';
-import { isAndroid } from '../../lib/methods/helpers';
-import { themes } from '../../lib/constants';
-import { TSupportedThemes, withTheme } from '../../theme';
+import { useTheme } from '../../theme';
import styles from './styles';
-import { IApplicationState } from '../../definitions';
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
-
-const margin = isAndroid ? 40 : 20;
-const maxSize = 400;
+import { FormTextInput } from '../../containers/TextInput/FormTextInput';
+import I18n from '../../i18n';
+import { sanitizeLikeString } from '../../lib/database/utils';
+import { emojis } from '../../containers/EmojiPicker/emojis';
+import database from '../../lib/database';
+import { debounce } from '../../lib/methods/helpers/debounce';
interface IReactionPickerProps {
message?: any;
show: boolean;
- isMasterDetail: boolean;
reactionClose: () => void;
onEmojiSelected: (shortname: string, id: string) => void;
width: number;
height: number;
- theme: TSupportedThemes;
}
-class ReactionPicker extends React.Component {
- shouldComponentUpdate(nextProps: IReactionPickerProps) {
- const { show, width, height } = this.props;
- return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
- }
+const MAX_EMOJIS_TO_DISPLAY = 20;
- onEmojiSelected = (_eventType: EventTypes, emoji?: string, shortname?: string) => {
+const ReactionPicker = React.memo(({ onEmojiSelected, message, reactionClose }: IReactionPickerProps) => {
+ const { colors } = useTheme();
+ const [searchText, setSearchText] = React.useState('');
+ const [searchedEmojis, setSearchedEmojis] = React.useState([]);
+ const [searching, setSearching] = React.useState(false);
+
+ const handleTextChange = (text: string) => {
+ setSearching(text !== '');
+ setSearchText(text);
+ searchEmojis(text);
+ };
+
+ const searchEmojis = debounce(async (keyword: string) => {
+ const likeString = sanitizeLikeString(keyword);
+ const whereClause = [];
+ if (likeString) {
+ whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
+ }
+ const db = database.active;
+ const customEmojisCollection = db.get('custom_emojis');
+ const customEmojis = await (await customEmojisCollection.query(...whereClause).fetch()).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];
+ setSearchedEmojis(mergedEmojis);
+ }, 300);
+
+ const handleEmojiSelect = (_eventType: EventTypes, emoji?: string, shortname?: string) => {
// standard emojis: `emoji` is unicode and `shortname` is :joy:
// custom emojis: only `emoji` is returned with shortname type (:joy:)
// to set reactions, we need shortname type
- const { onEmojiSelected, message } = this.props;
if (message) {
// @ts-ignore
onEmojiSelected(shortname || emoji, message.id);
}
+ reactionClose();
};
- render() {
- const { width, height, show, reactionClose, isMasterDetail, theme } = this.props;
-
- let widthStyle = width - margin;
- let heightStyle = Math.min(width, height) - margin * 2;
-
- if (isMasterDetail) {
- widthStyle = maxSize;
- heightStyle = maxSize;
- }
-
- return show ? (
-
-
-
-
-
- ) : null;
- }
-}
-
-const mapStateToProps = (state: IApplicationState) => ({
- isMasterDetail: state.app.isMasterDetail
+ return (
+
+
+ handleTextChange('')}
+ iconRight={'search'}
+ />
+
+
+
+ );
});
-export default connect(mapStateToProps)(withTheme(ReactionPicker));
+export default ReactionPicker;
diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx
index 4b7213f80..dda63b342 100644
--- a/app/views/RoomView/index.tsx
+++ b/app/views/RoomView/index.tsx
@@ -828,12 +828,32 @@ class RoomView extends React.Component {
this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false });
};
+ showReactionPicker = () => {
+ const { showActionSheet, width, height } = this.props;
+ const { reacting, selectedMessage } = this.state;
+ showActionSheet &&
+ showActionSheet({
+ children: (
+
+ ),
+ snaps: [400, '100%']
+ });
+ };
+
onReactionInit = (message: TAnyMessageModel) => {
- this.setState({ selectedMessage: message, reacting: true });
+ this.setState({ selectedMessage: message }, this.showReactionPicker);
};
onReactionClose = () => {
- this.setState({ selectedMessage: undefined, reacting: false });
+ const { hideActionSheet } = this.props;
+ this.setState({ selectedMessage: undefined, reacting: false }, hideActionSheet);
};
onMessageLongPress = (message: TAnyMessageModel) => {
@@ -1493,8 +1513,8 @@ class RoomView extends React.Component {
render() {
console.count(`${this.constructor.name}.render calls`);
- const { room, selectedMessage, loading, reacting } = this.state;
- const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props;
+ const { room, loading } = this.state;
+ const { user, baseUrl, theme, navigation, Hide_System_Messages, width, serverVersion } = this.props;
const { rid, t } = room;
let sysMes;
let bannerClosed;
@@ -1526,15 +1546,6 @@ class RoomView extends React.Component {
/>
{this.renderFooter()}
{this.renderActions()}
-
diff --git a/app/views/RoomView/styles.ts b/app/views/RoomView/styles.ts
index 4f84d8f7a..c7b3d3b0f 100644
--- a/app/views/RoomView/styles.ts
+++ b/app/views/RoomView/styles.ts
@@ -15,9 +15,7 @@ export default StyleSheet.create({
marginVertical: 15
},
reactionPickerContainer: {
- borderRadius: 4,
- flexDirection: 'column',
- overflow: 'hidden'
+ height: '100%'
},
bannerContainer: {
paddingVertical: 12,
@@ -64,5 +62,12 @@ export default StyleSheet.create({
previewMode: {
fontSize: 16,
...sharedStyles.textMedium
+ },
+ searchbarContainer: {
+ marginBottom: 10,
+ paddingHorizontal: 15
+ },
+ reactionPickerSearchbar: {
+ paddingHorizontal: 20
}
});