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 { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const EMOJI_SIZE = 50;
const MAX_EMOJI_SIZE = 50;
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
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) => (
<TouchableOpacity
activeOpacity={0.7}
@ -34,7 +36,7 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
{renderEmoji(emoji, emojiSize, baseUrl)}
</TouchableOpacity>
);
@ -42,14 +44,10 @@ const EmojiCategory = React.memo(({ baseUrl, onEmojiSelected, emojis, width, ...
return null;
}
const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return (
<FlatList
contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes
key={`emoji-category-${width}`}
key={`emoji-category-${numColumns}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
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 { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native';
import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { useTheme } from '../../theme';
interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
}
import { ITabBarProps } from './interfaces';
const TabBar = React.memo(({ activeTab, tabs, goToPage, tabEmojiStyle }: ITabBarProps) => {
const { colors } = useTheme();

View File

@ -1,11 +1,12 @@
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 orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import Footer from './Footer';
import styles from './styles';
import categories from './categories';
import database from '../../lib/database';
@ -16,13 +17,9 @@ import log from '../../lib/methods/helpers/log';
import { useTheme } from '../../theme';
import { IEmoji, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
import { useAppSelector } from '../../lib/hooks';
import { IEmojiPickerProps, EventTypes } from './interfaces';
interface IEmojiPickerProps {
onEmojiSelected: (emoji: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>;
}
const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPickerProps) => {
const EmojiPicker = React.memo(({ onItemClicked, tabEmojiStyle }: IEmojiPickerProps) => {
const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]);
const [show, setShow] = useState(false);
const [width, setWidth] = useState(null);
@ -54,12 +51,12 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
extension: emoji.extension,
isCustom: true
});
onEmojiSelected(`:${emoji.content}:`);
onItemClicked(EventTypes.EMOJI_PRESSED, `:${emoji.content}:`);
} else {
const content = emoji;
_addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
onItemClicked(EventTypes.EMOJI_PRESSED, shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
@ -112,7 +109,7 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
}
}: 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 = [];
if (i === 0) {
emojis = frequentlyUsed;
@ -129,13 +126,19 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
width={width}
baseUrl={baseUrl}
tabLabel={label}
tabsCount={tabsCount}
/>
);
};
const onBackspacePressed = () => onItemClicked(EventTypes.BACKSPACE_PRESSED);
if (!show) {
return null;
}
const tabsCount = frequentlyUsed.length === 0 ? categories.tabs.length - 1 : categories.tabs.length;
return (
<View onLayout={onLayout} style={{ flex: 1 }}>
<ScrollableTabView
@ -148,9 +151,10 @@ const EmojiPicker = React.memo(({ onEmojiSelected, tabEmojiStyle }: IEmojiPicker
{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)
: renderCategory(tab.category, i, tab.tabLabel, tabsCount)
)}
</ScrollableTabView>
<Footer onBackspacePressed={onBackspacePressed} />
</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: {
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 EmojiPicker from '../EmojiPicker';
import styles from './styles';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import {useTheme} from '../../theme'
import { EventTypes } from '../EmojiPicker/interfaces';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => {
const onEmojiSelected = (emoji: string) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
const EmojiKeyboard = () => {
const onItemClicked = (eventType: EventTypes, emoji: string | undefined) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
};
const {colors} = useTheme()
return (
<Provider store={store}>
<View
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
style={[styles.emojiKeyboardContainer, { borderTopColor: colors.borderColor }]}
testID='messagebox-keyboard-emoji'
>
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} />
<EmojiPicker onItemClicked={onItemClicked} />
</View>
</Provider>
);

View File

@ -59,6 +59,7 @@ import { hasPermission, debounce, isAndroid, isIOS, isTablet, compareServerVersi
import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types';
import { EventTypes } from '../EmojiPicker/interfaces';
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 { emoji } = params;
let newText = '';
// if messagebox has an active cursor
const { start, end } = this.selection;
const cursor = Math.max(start, end);
newText = `${text.substr(0, cursor)}${emoji}${text.substr(cursor)}`;
const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
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)}`;
const newCursor = cursor + emoji.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
}
};
getPermalink = async (message: any) => {
@ -1224,7 +1236,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onEmojiSelected}
onItemSelected={this.onItemSelected}
trackInteractive
requiresSameParentToManageScrollView
addBottomView

View File

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

View File

@ -9,6 +9,7 @@ import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import styles from './styles';
import { IApplicationState } from '../../definitions';
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
const margin = isAndroid ? 40 : 20;
const maxSize = 400;
@ -30,12 +31,13 @@ class ReactionPicker extends React.Component<IReactionPickerProps> {
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:
// 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);
}
};
@ -71,7 +73,7 @@ class ReactionPicker extends React.Component<IReactionPickerProps> {
]}
testID='reaction-picker'
>
<EmojiPicker theme={theme} onEmojiSelected={this.onEmojiSelected} />
<EmojiPicker onItemClicked={this.onEmojiSelected} />
</View>
</Modal>
) : null;