Add backspace button and same number of columns as category tabs

This commit is contained in:
Danish Ahmed Mirza 2022-06-25 02:21:19 +05:30 committed by Danish
parent 597a6836e6
commit 349c56ba45
10 changed files with 117 additions and 44 deletions

View File

@ -7,7 +7,7 @@ import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji'; import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const EMOJI_SIZE = 50; const MAX_EMOJI_SIZE = 50;
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => { const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji && emoji.isCustom) { if (emoji && emoji.isCustom) {
@ -26,7 +26,9 @@ const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
); );
}; };
const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...props }: IEmojiCategory) => { 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) => ( const renderItem = (emoji: IEmoji) => (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
@ -34,7 +36,7 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...
key={emoji && emoji.isCustom ? emoji.content : emoji} key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)} onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}> testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)} {renderEmoji(emoji, emojiSize, baseUrl)}
</TouchableOpacity> </TouchableOpacity>
); );
@ -42,14 +44,10 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...
return null; return null;
} }
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return ( return (
<FlatList <FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes // rerender FlatList in case of width changes
key={`emoji-category-${width}`} key={`emoji-category-${numColumns}`}
// @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

@ -0,0 +1,25 @@
import React from 'react';
import { View, TouchableOpacity } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler';
import { useTheme } from '../../theme';
import { CustomIcon } from '../CustomIcon';
import styles from './styles';
import { IFooterProps } from './interfaces';
const Footer = React.memo(({ onBackspacePressed }: IFooterProps) => {
const { colors } = useTheme();
return (
<View style={[styles.footerContainer, { backgroundColor: colors.bannerBackground }]}>
<BorderlessButton activeOpacity={0.7} onPress={() => console.log('Search!')} style={styles.footerButtonsContainer}>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
</BorderlessButton>
<TouchableOpacity activeOpacity={0.7} onPress={onBackspacePressed}>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' />
</TouchableOpacity>
</View>
);
});
export default Footer;

View File

@ -1,15 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { ITabBarProps } from './interfaces';
interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
}
const TabBar = React.memo(({ activeTab, tabs, goToPage, tabEmojiStyle }: ITabBarProps) => { const TabBar = React.memo(({ activeTab, tabs, goToPage, tabEmojiStyle }: ITabBarProps) => {
const { colors } = useTheme(); const { colors } = useTheme();

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { StyleProp, TextStyle, View } from 'react-native'; import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
import Footer from './Footer';
import styles from './styles'; import styles from './styles';
import categories from './categories'; import categories from './categories';
import database from '../../lib/database'; import database from '../../lib/database';
@ -16,13 +17,9 @@ import log from '../../lib/methods/helpers/log';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { IEmoji, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions'; import { IEmoji, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
import { useAppSelector } from '../../lib/hooks'; import { useAppSelector } from '../../lib/hooks';
import { IEmojiPickerProps, EventTypes } from './interfaces';
interface IEmojiPickerProps { const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerProps) => {
onEmojiSelected: (emoji: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>;
}
const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPickerProps) => {
const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]); const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]);
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [width, setWidth] = useState(null); const [width, setWidth] = useState(null);
@ -54,12 +51,12 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
extension: emoji.extension, extension: emoji.extension,
isCustom: true isCustom: true
}); });
onEmojiSelected(`:${emoji.content}:`); onItemClicked(EventTypes.EMOJI_PRESSED, `:${emoji.content}:`);
} else { } else {
const content = emoji; const content = emoji;
_addFrequentlyUsed({ content, isCustom: false }); _addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`; const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname); onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname);
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -112,7 +109,7 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
} }
}: any) => setWidth(width); }: any) => setWidth(width);
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => { const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string, tabsCount: number) => {
let emojis = []; let emojis = [];
if (i === 0) { if (i === 0) {
emojis = frequentlyUsed; emojis = frequentlyUsed;
@ -129,13 +126,19 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
width={width} width={width}
baseUrl={baseUrl} baseUrl={baseUrl}
tabLabel={label} tabLabel={label}
tabsCount={tabsCount}
/> />
); );
}; };
const onBackspacePressed = () => onItemClicked(EventTypes.BACKSPACE_PRESSED);
if (!show) { if (!show) {
return null; return null;
} }
const tabsCount = frequentlyUsed.length === 0 ? categories.tabs.length - 1 : categories.tabs.length;
return ( return (
<View onLayout={onLayout} style={{ flex: 1 }}> <View onLayout={onLayout} style={{ flex: 1 }}>
<ScrollableTabView <ScrollableTabView
@ -148,9 +151,10 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
{categories.tabs.map((tab: any, i) => {categories.tabs.map((tab: any, i) =>
i === 0 && frequentlyUsed.length === 0 i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab ? null // when no frequentlyUsed don't show the tab
: renderCategory(tab.category, i, tab.tabLabel) : renderCategory(tab.category, i, tab.tabLabel, tabsCount)
)} )}
</ScrollableTabView> </ScrollableTabView>
<Footer onBackspacePressed={onBackspacePressed} />
</View> </View>
); );
}); });

View File

@ -0,0 +1,22 @@
import { StyleProp, TextStyle } from 'react-native';
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed'
}
export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>;
}
export interface IFooterProps {
onBackspacePressed: () => void;
}
export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
}

View File

@ -54,5 +54,18 @@ export default StyleSheet.create({
}, },
customCategoryEmoji: { customCategoryEmoji: {
margin: 8 margin: 8
},
footerContainer: {
height: 45,
paddingHorizontal: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center'
},
footerButtonsContainer: {
height: 30,
width: 30,
justifyContent: 'center',
alignItems: 'center'
} }
}); });

View File

@ -6,21 +6,23 @@ import { Provider } from 'react-redux';
import store from '../../lib/store'; import store from '../../lib/store';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
import styles from './styles'; import styles from './styles';
import { themes } from '../../lib/constants'; import {useTheme} from '../../theme'
import { TSupportedThemes } from '../../theme'; import { EventTypes } from '../EmojiPicker/interfaces';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => { const EmojiKeyboard = () => {
const onEmojiSelected = (emoji: string) => { const onItemClicked = (eventType: EventTypes, emoji: string | undefined) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
}; };
const {colors} = useTheme()
return ( return (
<Provider store={store}> <Provider store={store}>
<View <View
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} style={[styles.emojiKeyboardContainer, { borderTopColor: colors.borderColor }]}
testID='messagebox-keyboard-emoji' testID='messagebox-keyboard-emoji'
> >
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} /> <EmojiPicker onItemClicked={onItemClicked} />
</View> </View>
</Provider> </Provider>
); );

View File

@ -59,6 +59,7 @@ import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersi
import { Services } from '../../lib/services'; 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';
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -577,18 +578,29 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
onEmojiSelected = (keyboardId: string, params: { emoji: string }) => { onItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: string }) => {
const { eventType, emoji } = params;
const { text } = this; const { text } = this;
const { emoji } = params;
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 charsToRemove = 1;
if (cursor > 1) {
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
const lastEmoji = text.substr(cursor - 2, cursor);
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)}`; newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
const newCursor = cursor + emoji.length; const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor }); this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true); this.setShowSend(true);
}
}; };
getPermalink = async (message: any) => { getPermalink = async (message: any) => {
@ -1224,7 +1236,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.onEmojiSelected} onItemSelected={this.onItemSelected}
trackInteractive trackInteractive
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
addBottomView addBottomView

View File

@ -35,6 +35,7 @@ export interface IEmojiCategory {
width: number | null; width: number | null;
style: StyleProp<ImageStyle>; style: StyleProp<ImageStyle>;
tabLabel: string; tabLabel: string;
tabsCount: number;
} }
export type TGetCustomEmoji = (name: string) => any; export type TGetCustomEmoji = (name: string) => any;

View File

@ -9,6 +9,7 @@ import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme'; import { TSupportedThemes, withTheme } from '../../theme';
import styles from './styles'; import styles from './styles';
import { IApplicationState } from '../../definitions'; import { IApplicationState } from '../../definitions';
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
const margin = isAndroid ? 40 : 20; const margin = isAndroid ? 40 : 20;
const maxSize = 400; const maxSize = 400;
@ -30,12 +31,13 @@ class ReactionPicker extends React.Component<IReactionPickerProps> {
return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height; return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
} }
onEmojiSelected = (emoji: string, shortname?: string) => { onEmojiSelected = (_eventType: EventTypes, emoji?: string, shortname?: string) => {
// standard emojis: `emoji` is unicode and `shortname` is :joy: // standard emojis: `emoji` is unicode and `shortname` is :joy:
// custom emojis: only `emoji` is returned with shortname type (:joy:) // custom emojis: only `emoji` is returned with shortname type (:joy:)
// to set reactions, we need shortname type // to set reactions, we need shortname type
const { onEmojiSelected, message } = this.props; const { onEmojiSelected, message } = this.props;
if (message) { if (message) {
// @ts-ignore
onEmojiSelected(shortname || emoji, message.id); onEmojiSelected(shortname || emoji, message.id);
} }
}; };
@ -71,7 +73,7 @@ class ReactionPicker extends React.Component<IReactionPickerProps> {
]} ]}
testID='reaction-picker' testID='reaction-picker'
> >
<EmojiPicker theme={theme} onEmojiSelected={this.onEmojiSelected} /> <EmojiPicker onItemClicked={this.onEmojiSelected} />
</View> </View>
</Modal> </Modal>
) : null; ) : null;