Add backspace button and same number of columns as category tabs
This commit is contained in:
parent
597a6836e6
commit
349c56ba45
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,6 +35,7 @@ export interface IEmojiCategory {
|
|||
width: number | null;
|
||||
style: StyleProp<ImageStyle>;
|
||||
tabLabel: string;
|
||||
tabsCount: number;
|
||||
}
|
||||
|
||||
export type TGetCustomEmoji = (name: string) => any;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue