[IMPROVE] Redesign emoji picker (#4328)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Danish Ahmed Mirza 2022-10-21 23:57:55 +05:30 committed by GitHub
parent ec03f49b2d
commit 1486204546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1156 additions and 823 deletions

View File

@ -3,7 +3,13 @@ import { Provider } from 'react-redux';
import { themes } from '../app/lib/constants';
import MessageContext from '../app/containers/message/Context';
import { selectServerRequest } from '../app/actions/server';
import { mockedStore as store } from '../app/reducers/mockedStore';
import { setUser } from '../app/actions/login';
const baseUrl = 'https://open.rocket.chat';
store.dispatch(selectServerRequest(baseUrl));
store.dispatch(setUser({ id: 'abc', username: 'rocket.cat', name: 'Rocket Cat' }));
export const decorators = [
Story => (
@ -15,7 +21,7 @@ export const decorators = [
username: 'diego.mello',
token: 'abc'
},
baseUrl: 'https://open.rocket.chat',
baseUrl,
onPress: () => {},
onLongPress: () => {},
reactionInit: () => {},

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Rocket.Cat\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip With Short Text 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Short\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":false},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar\\"]}]},{\\"type\\":\\"Text\\",\\"props\\":{\\"selectable\\":false,\\"allowFontScaling\\":false,\\"style\\":[{\\"fontSize\\":16,\\"color\\":\\"#6C727A\\"},null,{\\"fontFamily\\":\\"custom\\",\\"fontWeight\\":\\"normal\\",\\"fontStyle\\":\\"normal\\"},{}]},\\"children\\":[\\"\\"]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Avatar And Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Avatar and Icon\\"]}]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`;
exports[`Storyshots Chip Chip Without Icon 1`] = `"{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flex\\":1,\\"alignItems\\":\\"flex-start\\",\\"padding\\":16}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"accessible\\":true,\\"accessibilityState\\":{\\"disabled\\":true},\\"focusable\\":true,\\"style\\":[{\\"paddingHorizontal\\":8,\\"marginRight\\":8,\\"borderRadius\\":2,\\"justifyContent\\":\\"center\\",\\"maxWidth\\":192},{\\"backgroundColor\\":\\"#efeff4\\"},null],\\"collapsable\\":false},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"flexDirection\\":\\"row\\",\\"alignItems\\":\\"center\\"}},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4},{\\"marginRight\\":8,\\"marginVertical\\":8}],\\"testID\\":\\"avatar\\"},\\"children\\":[{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":[{\\"overflow\\":\\"hidden\\"},{\\"width\\":28,\\"height\\":28,\\"borderRadius\\":4}]},\\"children\\":[{\\"type\\":\\"FastImageView\\",\\"props\\":{\\"style\\":{\\"position\\":\\"absolute\\",\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"bottom\\":0},\\"source\\":{\\"uri\\":\\"https://open.rocket.chat/avatar/rocket.cat?format=png&size=56\\",\\"headers\\":{\\"User-Agent\\":\\"RC Mobile; ios unknown; vunknown (unknown)\\"},\\"priority\\":\\"high\\"},\\"resizeMode\\":\\"cover\\"},\\"children\\":null}]}]},{\\"type\\":\\"View\\",\\"props\\":{\\"style\\":{\\"marginRight\\":8,\\"maxWidth\\":120}},\\"children\\":[{\\"type\\":\\"Text\\",\\"props\\":{\\"style\\":[{\\"fontSize\\":16,\\"textAlign\\":\\"left\\",\\"backgroundColor\\":\\"transparent\\",\\"fontFamily\\":\\"Inter\\",\\"fontWeight\\":\\"500\\"},{\\"color\\":\\"#2f343d\\"}],\\"numberOfLines\\":1},\\"children\\":[\\"Without Icon\\"]}]}]}]}]}"`;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,10 +18,10 @@ const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MIN_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
export const ACTION_SHEET_ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
duration: ACTION_SHEET_ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
@ -140,7 +140,7 @@ 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
// We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
{...androidTablet}
>

View File

@ -52,7 +52,11 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children }: I
/>
);
}
return <BottomSheetView style={styles.contentContainer}>{children}</BottomSheetView>;
return (
<BottomSheetView testID='action-sheet' style={styles.contentContainer}>
{children}
</BottomSheetView>
);
});
export default BottomSheetContent;

View File

@ -1 +1,2 @@
export * from './Provider';
export * from './ActionSheet';

View File

@ -43,9 +43,7 @@ const Avatar = React.memo(
let image;
if (emoji) {
image = (
<Emoji baseUrl={server} getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />
);
image = <Emoji getCustomEmoji={getCustomEmoji} isMessageContainsOnlyEmoji literal={emoji} style={avatarStyle} />;
} else {
let uri = avatar;
if (!isStatic) {

View File

@ -1,24 +1,30 @@
import React from 'react';
import FastImage from 'react-native-fast-image';
import { StyleProp } from 'react-native';
import FastImage, { ImageStyle } from 'react-native-fast-image';
import { ICustomEmoji } from '../../definitions/IEmoji';
import { useAppSelector } from '../../lib/hooks';
import { ICustomEmoji } from '../../definitions';
interface ICustomEmojiProps {
emoji: ICustomEmoji;
style: StyleProp<ImageStyle>;
}
const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => (
<FastImage
style={style}
source={{
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
),
(prevProps, nextProps) => {
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
}
({ emoji, style }: ICustomEmojiProps) => {
const baseUrl = useAppSelector(state => state.share.server.server || state.server.server);
return (
<FastImage
style={style}
source={{
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
);
},
() => true
);
export default CustomEmoji;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Text } from 'react-native';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { IEmoji } from '../../definitions/IEmoji';
interface IEmojiProps {
emoji: IEmoji;
}
export const Emoji = ({ emoji }: IEmojiProps): React.ReactElement => {
if (typeof emoji === 'string') {
return <Text style={styles.categoryEmoji}>{shortnameToUnicode(`:${emoji}:`)}</Text>;
}
return <CustomEmoji style={styles.customCategoryEmoji} emoji={emoji} />;
};

View File

@ -1,75 +1,45 @@
import React from 'react';
import { FlatList, Text, TouchableOpacity } from 'react-native';
import { useWindowDimensions } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import { EMOJI_BUTTON_SIZE } from './styles';
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
import { IEmoji } from '../../definitions/IEmoji';
import { PressableEmoji } from './PressableEmoji';
const EMOJI_SIZE = 50;
interface IEmojiCategoryProps {
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
tabLabel?: string; // needed for react-native-scrollable-tab-view only
}
const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji && emoji.isCustom) {
return (
<CustomEmoji
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
emoji={emoji}
baseUrl={baseUrl}
/>
);
const EmojiCategory = ({ onEmojiSelected, emojis }: IEmojiCategoryProps): React.ReactElement | null => {
const { width } = useWindowDimensions();
const numColumns = Math.trunc(width / EMOJI_BUTTON_SIZE);
const marginHorizontal = (width % EMOJI_BUTTON_SIZE) / 2;
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={onEmojiSelected} />;
if (!width) {
return null;
}
return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${emoji}:`)}
</Text>
<FlatList
// needed to update the numColumns when the width changes
key={`emoji-category-${width}`}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
data={emojis}
renderItem={renderItem}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
contentContainerStyle={{ marginHorizontal }}
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
};
class EmojiCategory extends React.Component<IEmojiCategory> {
renderItem(emoji: IEmoji) {
const { baseUrl, onEmojiSelected } = this.props;
return (
<TouchableOpacity
activeOpacity={0.7}
// @ts-ignore
key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)}
testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}
>
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity>
);
}
render() {
const { emojis, width } = this.props;
if (!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}`}
// @ts-ignore
keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis}
extraData={this.props}
renderItem={({ item }) => this.renderItem(item)}
numColumns={numColumns}
initialNumToRender={45}
removeClippedSubviews
{...scrollPersistTaps}
keyboardDismissMode={'none'}
/>
);
}
}
export default EmojiCategory;

View File

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { StyleSheet, TextInputProps } from 'react-native';
import { FormTextInput } from '../TextInput/FormTextInput';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { isIOS } from '../../lib/methods/helpers';
const styles = StyleSheet.create({
input: {
height: 32,
borderWidth: 0,
paddingVertical: 0,
borderRadius: 4
},
textInputContainer: {
marginBottom: 0
}
});
interface IEmojiSearchBarProps {
onBlur?: TextInputProps['onBlur'];
onChangeText: TextInputProps['onChangeText'];
bottomSheet?: boolean;
}
export const EmojiSearch = ({ onBlur, onChangeText, bottomSheet }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const handleTextChange = (text: string) => {
setSearchText(text);
if (onChangeText) {
onChangeText(text);
}
};
return (
<FormTextInput
autoCapitalize='none'
autoCorrect={false}
autoComplete='off'
returnKeyType='search'
textContentType='none'
blurOnSubmit
placeholder={I18n.t('Search_emoji')}
placeholderTextColor={colors.auxiliaryText}
underlineColorAndroid='transparent'
onChangeText={handleTextChange}
inputStyle={[styles.input, { backgroundColor: colors.textInputSecondaryBackground }]}
containerStyle={styles.textInputContainer}
value={searchText}
onClearInput={() => handleTextChange('')}
onBlur={onBlur}
iconRight={'search'}
testID='emoji-searchbar-input'
bottomSheet={bottomSheet && isIOS}
autoFocus={!bottomSheet} // focus on input when not in reaction picker
/>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { View, Pressable } from 'react-native';
import { useTheme } from '../../theme';
import { CustomIcon } from '../CustomIcon';
import styles from './styles';
import { IFooterProps } from './interfaces';
const BUTTON_HIT_SLOP = { top: 15, right: 15, bottom: 15, left: 15 };
const Footer = ({ onSearchPressed, onBackspacePressed }: IFooterProps): React.ReactElement => {
const { colors } = useTheme();
return (
<View style={[styles.footerContainer, { borderTopColor: colors.borderColor }]}>
<Pressable
onPress={onSearchPressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-search'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='search' />
</Pressable>
<Pressable
onPress={onBackspacePressed}
hitSlop={BUTTON_HIT_SLOP}
style={({ pressed }) => [styles.footerButtonsContainer, { opacity: pressed ? 0.7 : 1 }]}
testID='emoji-picker-backspace'
>
<CustomIcon color={colors.auxiliaryTintColor} size={24} name='backspace' />
</Pressable>
</View>
);
};
export default Footer;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Pressable } from 'react-native';
import styles, { EMOJI_BUTTON_SIZE } from './styles';
import { IEmoji } from '../../definitions/IEmoji';
import { useTheme } from '../../theme';
import { isIOS } from '../../lib/methods/helpers';
import { Emoji } from './Emoji';
export const PressableEmoji = ({ emoji, onPress }: { emoji: IEmoji; onPress: (emoji: IEmoji) => void }): React.ReactElement => {
const { colors } = useTheme();
return (
<Pressable
key={typeof emoji === 'string' ? emoji : emoji.name}
onPress={() => onPress(emoji)}
testID={`emoji-${typeof emoji === 'string' ? emoji : emoji.name}`}
android_ripple={{ color: colors.bannerBackground, borderless: true, radius: EMOJI_BUTTON_SIZE / 2 }}
style={({ pressed }: { pressed: boolean }) => [
styles.emojiButton,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<Emoji emoji={emoji} />
</Pressable>
);
};

View File

@ -1,56 +1,42 @@
import React from 'react';
import { StyleProp, Text, TextStyle, TouchableOpacity, View } from 'react-native';
import { Pressable, View } from 'react-native';
import styles from './styles';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { useTheme } from '../../theme';
import { ITabBarProps } from './interfaces';
import { isIOS } from '../../lib/methods/helpers';
import { CustomIcon } from '../CustomIcon';
interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: string[];
tabEmojiStyle: StyleProp<TextStyle>;
theme: TSupportedThemes;
}
const TabBar = ({ activeTab, tabs, goToPage }: ITabBarProps): React.ReactElement => {
const { colors } = useTheme();
export default class TabBar extends React.Component<ITabBarProps> {
shouldComponentUpdate(nextProps: ITabBarProps) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
return (
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<Pressable
key={tab}
onPress={() => goToPage?.(i)}
testID={`emoji-picker-tab-${tab}`}
android_ripple={{ color: colors.bannerBackground }}
style={({ pressed }: { pressed: boolean }) => [
styles.tab,
{
backgroundColor: isIOS && pressed ? colors.bannerBackground : 'transparent'
}
]}
>
<CustomIcon name={tab} size={24} color={activeTab === i ? colors.tintColor : colors.auxiliaryTintColor} />
<View
style={
activeTab === i
? [styles.activeTabLine, { backgroundColor: colors.tintColor }]
: [styles.tabLine, { backgroundColor: colors.borderColor }]
}
/>
</Pressable>
))}
</View>
);
};
render() {
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
return (
<View style={styles.tabsContainer}>
{tabs?.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => {
if (goToPage) {
goToPage(i);
}
}}
style={styles.tab}
testID={`reaction-picker-${tab}`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}
export default TabBar;

View File

@ -1,155 +1,44 @@
import React, { Component } from 'react';
import { StyleProp, TextStyle, View } from 'react-native';
import React from 'react';
import { View } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal';
import { connect } from 'react-redux';
import orderBy from 'lodash/orderBy';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { ImageStyle } from 'react-native-fast-image';
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';
import { emojisByCategory } from './emojis';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import log from '../../lib/methods/helpers/log';
import { themes } from '../../lib/constants';
import { TSupportedThemes } from '../../theme';
import { IEmoji, TGetCustomEmoji, IApplicationState, ICustomEmojis, TFrequentlyUsedEmojiModel } from '../../definitions';
import { categories, emojisByCategory } from '../../lib/constants';
import { useTheme } from '../../theme';
import { IEmoji, ICustomEmojis } from '../../definitions';
import { useAppSelector, useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed } from '../../lib/methods';
import { IEmojiPickerProps, EventTypes } from './interfaces';
interface IEmojiPickerProps {
isMessageContainsOnlyEmoji?: boolean;
getCustomEmoji?: TGetCustomEmoji;
baseUrl: string;
customEmojis: ICustomEmojis;
style?: StyleProp<ImageStyle>;
theme: TSupportedThemes;
onEmojiSelected: (emoji: string, shortname?: string) => void;
tabEmojiStyle?: StyleProp<TextStyle>;
}
const EmojiPicker = ({
onItemClicked,
isEmojiKeyboard = false,
searching = false,
searchedEmojis = []
}: IEmojiPickerProps): React.ReactElement | null => {
const { colors } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji();
interface IEmojiPickerState {
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
customEmojis: any;
show: boolean;
width: number | null;
}
const allCustomEmojis: ICustomEmojis = useAppSelector(
state => state.customEmojis,
() => true
);
const customEmojis = Object.keys(allCustomEmojis)
.filter(item => item === allCustomEmojis[item].name)
.map(item => ({
name: allCustomEmojis[item].name,
extension: allCustomEmojis[item].extension
}));
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
constructor(props: IEmojiPickerProps) {
super(props);
const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name)
.map(item => ({
content: props.customEmojis[item].name,
extension: props.customEmojis[item].extension,
isCustom: true
}));
this.state = {
frequentlyUsed: [],
customEmojis,
show: false,
width: null
};
}
async componentDidMount() {
await this.updateFrequentlyUsed();
this.setState({ show: true });
}
shouldComponentUpdate(nextProps: IEmojiPickerProps, nextState: IEmojiPickerState) {
const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props;
if (nextProps.theme !== theme) {
return true;
}
if (nextState.show !== show) {
return true;
}
if (nextState.width !== width) {
return true;
}
if (!dequal(nextState.frequentlyUsed, frequentlyUsed)) {
return true;
}
return false;
}
onEmojiSelected = (emoji: IEmoji) => {
try {
const { onEmojiSelected } = this.props;
if (emoji.isCustom) {
this._addFrequentlyUsed({
content: emoji.content,
extension: emoji.extension,
isCustom: true
});
onEmojiSelected(`:${emoji.content}:`);
} else {
const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname);
}
} catch (e) {
log(e);
}
const handleEmojiSelect = (emoji: IEmoji) => {
onItemClicked(EventTypes.EMOJI_PRESSED, emoji);
addFrequentlyUsed(emoji);
};
_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;
});
}
});
});
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom };
}
return shortnameToUnicode(`${item.content}`);
});
this.setState({ frequentlyUsed });
};
onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category: keyof typeof emojisByCategory, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props;
const renderCategory = (category: keyof typeof emojisByCategory, i: number, label: string) => {
let emojis = [];
if (i === 0) {
emojis = frequentlyUsed;
@ -158,49 +47,40 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
} else {
emojis = emojisByCategory[category];
}
return (
<EmojiCategory
emojis={emojis}
onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
width={width}
baseUrl={baseUrl}
tabLabel={label}
/>
);
}
render() {
const { show, frequentlyUsed } = this.state;
const { tabEmojiStyle, theme } = this.props;
if (!show) {
if (!emojis.length) {
return null;
}
return (
<View onLayout={this.onLayout} style={{ flex: 1 }}>
return <EmojiCategory emojis={emojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} tabLabel={label} />;
};
if (!loaded) {
return null;
}
return (
<View style={styles.emojiPickerContainer}>
{searching ? (
<EmojiCategory emojis={searchedEmojis} onEmojiSelected={(emoji: IEmoji) => handleEmojiSelect(emoji)} />
) : (
<ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
renderTabBar={() => <TabBar />}
contentProps={{
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none'
}}
style={{ backgroundColor: themes[theme].focusedBackground }}
style={{ backgroundColor: colors.messageboxBackground }}
>
{categories.tabs.map((tab: any, i) =>
i === 0 && frequentlyUsed.length === 0
? null // when no frequentlyUsed don't show the tab
: this.renderCategory(tab.category, i, tab.tabLabel)
)}
{categories.tabs.map((tab: any, i) => renderCategory(tab.category, i, tab.tabLabel))}
</ScrollableTabView>
</View>
);
}
}
)}
{isEmojiKeyboard && (
<Footer
onSearchPressed={() => onItemClicked(EventTypes.SEARCH_PRESSED)}
onBackspacePressed={() => onItemClicked(EventTypes.BACKSPACE_PRESSED)}
/>
)}
</View>
);
};
const mapStateToProps = (state: IApplicationState) => ({
customEmojis: state.customEmojis,
baseUrl: state.share.server.server || state.server.server
});
export default connect(mapStateToProps)(EmojiPicker);
export default EmojiPicker;

View File

@ -0,0 +1,26 @@
import { TIconsName } from '../CustomIcon';
import { IEmoji } from '../../definitions';
export enum EventTypes {
EMOJI_PRESSED = 'emojiPressed',
BACKSPACE_PRESSED = 'backspacePressed',
SEARCH_PRESSED = 'searchPressed'
}
export interface IEmojiPickerProps {
onItemClicked: (event: EventTypes, emoji?: IEmoji) => void;
isEmojiKeyboard?: boolean;
searching?: boolean;
searchedEmojis?: IEmoji[];
}
export interface IFooterProps {
onBackspacePressed: () => void;
onSearchPressed: () => void;
}
export interface ITabBarProps {
goToPage?: (page: number) => void;
activeTab?: number;
tabs?: TIconsName[];
}

View File

@ -2,20 +2,23 @@ import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const EMOJI_BUTTON_SIZE = 44;
export const EMOJI_SIZE = EMOJI_BUTTON_SIZE - 16;
export default StyleSheet.create({
container: {
flex: 1
},
tabsContainer: {
height: 45,
flexDirection: 'row',
paddingTop: 5
height: EMOJI_BUTTON_SIZE,
flexDirection: 'row'
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 10
paddingVertical: 10,
width: EMOJI_BUTTON_SIZE
},
tabEmoji: {
fontSize: 20,
@ -33,7 +36,6 @@ export default StyleSheet.create({
left: 0,
right: 0,
height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0
},
categoryContainer: {
@ -49,10 +51,34 @@ export default StyleSheet.create({
},
categoryEmoji: {
...sharedStyles.textAlignCenter,
textAlignVertical: 'center',
fontSize: EMOJI_SIZE,
backgroundColor: 'transparent',
color: '#ffffff'
},
customCategoryEmoji: {
margin: 8
}
height: EMOJI_SIZE,
width: EMOJI_SIZE
},
emojiButton: {
alignItems: 'center',
justifyContent: 'center',
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE
},
footerContainer: {
height: EMOJI_BUTTON_SIZE,
paddingHorizontal: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderTopWidth: 1
},
footerButtonsContainer: {
height: EMOJI_BUTTON_SIZE,
width: EMOJI_BUTTON_SIZE,
justifyContent: 'center',
alignItems: 'center'
},
emojiPickerContainer: { flex: 1 }
});

View File

@ -1,32 +1,29 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { TSupportedThemes, useTheme } from '../../theme';
import { themes } from '../../lib/constants';
import { CustomIcon } from '../CustomIcon';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
import { addFrequentlyUsed } from '../../lib/methods';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { TAnyMessageModel, TFrequentlyUsedEmojiModel } from '../../definitions';
import { IEmoji, TAnyMessageModel } from '../../definitions';
import Touch from '../Touch';
type TItem = TFrequentlyUsedEmojiModel | string;
export interface IHeader {
handleReaction: (emoji: TItem, message: TAnyMessageModel) => void;
server: string;
handleReaction: (emoji: IEmoji, message: TAnyMessageModel) => void;
message: TAnyMessageModel;
isMasterDetail: boolean;
}
type TOnReaction = ({ emoji }: { emoji: TItem }) => void;
type TOnReaction = ({ emoji }: { emoji: IEmoji }) => void;
interface THeaderItem {
item: TItem;
item: IEmoji;
onReaction: TOnReaction;
server: string;
theme: TSupportedThemes;
}
@ -64,30 +61,19 @@ const styles = StyleSheet.create({
}
});
const keyExtractor = (item: TItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
return (emojiModel.id ? emojiModel.content : item) as string;
};
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = ({ item, onReaction, server, theme }: THeaderItem) => {
const emojiModel = item as TFrequentlyUsedEmojiModel;
const emoji = (emojiModel.id ? emojiModel.content : item) as string;
return (
<Touch
testID={`message-actions-emoji-${emoji}`}
onPress={() => onReaction({ emoji: `:${emoji}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
>
{emojiModel?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={emojiModel} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${emoji}:`)}</Text>
)}
</Touch>
);
};
const HeaderItem = ({ item, onReaction, theme }: THeaderItem) => (
<Touch
testID={`message-actions-emoji-${item}`}
onPress={() => onReaction({ emoji: item })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
>
{typeof item === 'string' ? (
<Text style={styles.headerIcon}>{shortnameToUnicode(`:${item}:`)}</Text>
) : (
<CustomEmoji style={styles.customEmoji} emoji={item} />
)}
</Touch>
);
const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
<Touch
@ -99,49 +85,35 @@ const HeaderFooter = ({ onReaction, theme }: THeaderFooter) => (
</Touch>
);
const Header = React.memo(({ handleReaction, server, message, isMasterDetail }: IHeader) => {
const [items, setItems] = useState<TItem[]>([]);
const Header = React.memo(({ handleReaction, message, isMasterDetail }: IHeader) => {
const { width, height } = useDimensions();
const { theme } = useTheme();
const { frequentlyUsed, loaded } = useFrequentlyUsedEmoji(true);
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = Math.trunc(size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1);
// TODO: create custom hook to re-render based on screen size
const setEmojis = async () => {
try {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis: TItem[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
} catch {
// Do nothing
}
const onReaction: TOnReaction = ({ emoji }) => {
handleReaction(emoji, message);
addFrequentlyUsed(emoji);
};
useEffect(() => {
setEmojis();
}, []);
const onReaction: TOnReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = ({ item }: { item: TItem }) => (
<HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />
);
const renderItem = ({ item }: { item: IEmoji }) => <HeaderItem item={item} onReaction={onReaction} theme={theme} />;
const renderFooter = () => <HeaderFooter onReaction={onReaction} theme={theme} />;
if (!loaded) {
return null;
}
return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList
data={items}
data={frequentlyUsed.slice(0, quantity)}
renderItem={renderItem}
ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
showsHorizontalScrollIndicator={false}
scrollEnabled={false}
horizontal

View File

@ -12,10 +12,10 @@ import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../lib/methods/helpers/events';
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATION } from '../ActionSheet';
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods';
import { hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
@ -26,7 +26,7 @@ export interface IMessageActionsProps {
user: Pick<ILoggedUser, 'id'>;
editInit: (message: TAnyMessageModel) => void;
reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: string, messageId: string) => void;
onReactionPress: (shortname: IEmoji, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void;
isMasterDetail: boolean;
isReadOnly: boolean;
@ -37,7 +37,6 @@ export interface IMessageActionsProps {
Message_AllowPinning?: boolean;
Message_AllowStarring?: boolean;
Message_Read_Receipt_Store_Users?: boolean;
server: string;
editMessagePermission?: string[];
deleteMessagePermission?: string[];
forceDeleteMessagePermission?: string[];
@ -60,7 +59,6 @@ const MessageActions = React.memo(
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
@ -261,12 +259,10 @@ const MessageActions = React.memo(
const handleReaction: IHeader['handleReaction'] = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
// TODO: evaluate unification with IEmoji
onReactionPress(shortname as any, message.id);
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
setTimeout(() => reactionInit(message), ACTION_SHEET_ANIMATION_DURATION);
}
// close actionSheet when click at header
hideActionSheet();
};
@ -460,7 +456,7 @@ const MessageActions = React.memo(
headerHeight: HEADER_HEIGHT,
customHeader:
!isReadOnly || room.reactWhenReadOnly ? (
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
<Header handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null
});
};

View File

@ -6,22 +6,31 @@ 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 { ThemeContext, TSupportedThemes } from '../../theme';
import { EventTypes } from '../EmojiPicker/interfaces';
import { IEmoji } from '../../definitions';
import { colors } from '../../lib/constants';
const EmojiKeyboard = ({ theme }: { theme: TSupportedThemes }) => {
const onEmojiSelected = (emoji: string) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
const onItemClicked = (eventType: EventTypes, emoji?: IEmoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { eventType, emoji });
};
return (
<Provider store={store}>
<View
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
testID='messagebox-keyboard-emoji'
<ThemeContext.Provider
value={{
theme,
colors: colors[theme]
}}
>
<EmojiPicker onEmojiSelected={onEmojiSelected} theme={theme} />
</View>
<View
style={[styles.emojiKeyboardContainer, { borderTopColor: colors[theme].borderColor }]}
testID='messagebox-keyboard-emoji'
>
<EmojiPicker onItemClicked={onItemClicked} isEmojiKeyboard={true} />
</View>
</ThemeContext.Provider>
</Provider>
);
};

View File

@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { useTheme } from '../../theme';
import I18n from '../../i18n';
import { CustomIcon } from '../CustomIcon';
import { IEmoji } from '../../definitions';
import { useFrequentlyUsedEmoji } from '../../lib/hooks';
import { addFrequentlyUsed, searchEmojis } from '../../lib/methods';
import { useDebounce } from '../../lib/methods/helpers';
import sharedStyles from '../../views/Styles';
import { PressableEmoji } from '../EmojiPicker/PressableEmoji';
import { EmojiSearch } from '../EmojiPicker/EmojiSearch';
import { EMOJI_BUTTON_SIZE } from '../EmojiPicker/styles';
import { events, logEvent } from '../../lib/methods/helpers/log';
const BUTTON_HIT_SLOP = { top: 4, right: 4, bottom: 4, left: 4 };
const styles = StyleSheet.create({
listContainer: {
height: EMOJI_BUTTON_SIZE,
margin: 8,
flexGrow: 1
},
container: {
borderTopWidth: 1
},
searchContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
marginBottom: 12
},
backButton: {
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
emptyText: {
...sharedStyles.textRegular,
fontSize: 16
},
inputContainer: {
flex: 1
}
});
interface IEmojiSearchBarProps {
openEmoji: () => void;
closeEmoji: () => void;
onEmojiSelected: (emoji: IEmoji) => void;
}
const EmojiSearchBar = ({ openEmoji, closeEmoji, onEmojiSelected }: IEmojiSearchBarProps): React.ReactElement => {
const { colors } = useTheme();
const [searchText, setSearchText] = useState<string>('');
const { frequentlyUsed } = useFrequentlyUsedEmoji(true);
const [emojis, setEmojis] = useState<IEmoji[]>([]);
const handleTextChange = useDebounce(async (text: string) => {
logEvent(events.MB_SB_EMOJI_SEARCH);
setSearchText(text);
const result = await searchEmojis(text);
setEmojis(result);
}, 300);
const handleEmojiSelected = (emoji: IEmoji) => {
logEvent(events.MB_SB_EMOJI_SELECTED);
onEmojiSelected(emoji);
addFrequentlyUsed(emoji);
};
const renderItem = ({ item }: { item: IEmoji }) => <PressableEmoji emoji={item} onPress={handleEmojiSelected} />;
return (
<View style={[styles.container, { borderTopColor: colors.borderColor, backgroundColor: colors.messageboxBackground }]}>
<FlatList
horizontal
data={searchText ? emojis : frequentlyUsed}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={() => (
<View style={styles.emptyContainer} testID='no-results-found'>
<Text style={[styles.emptyText, { color: colors.auxiliaryText }]}>{I18n.t('No_results_found')}</Text>
</View>
)}
keyExtractor={item => (typeof item === 'string' ? item : item.name)}
contentContainerStyle={styles.listContainer}
keyboardShouldPersistTaps='always'
/>
<View style={styles.searchContainer}>
<Pressable
style={({ pressed }: { pressed: boolean }) => [styles.backButton, { opacity: pressed ? 0.7 : 1 }]}
onPress={openEmoji}
hitSlop={BUTTON_HIT_SLOP}
testID='openback-emoji-keyboard'
>
<CustomIcon name='chevron-left' size={24} color={colors.auxiliaryTintColor} />
</Pressable>
<View style={styles.inputContainer}>
<EmojiSearch onBlur={closeEmoji} onChangeText={handleTextChange} />
</View>
</View>
</View>
);
};
export default EmojiSearchBar;

View File

@ -1,10 +1,9 @@
import React, { useContext } from 'react';
import React from 'react';
import { Text } from 'react-native';
import { IEmoji } from '../../../definitions/IEmoji';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MessageboxContext from '../Context';
import styles from '../styles';
interface IMessageBoxMentionEmoji {
@ -12,13 +11,10 @@ interface IMessageBoxMentionEmoji {
}
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
const context = useContext(MessageboxContext);
const { baseUrl } = context;
if (item.name) {
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
if (typeof item === 'string') {
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
}
return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} />;
};
export default MentionEmoji;

View File

@ -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;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Alert, Keyboard, NativeModules, Text, View } from 'react-native';
import { Alert, Keyboard, NativeModules, Text, View, BackHandler } 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';
@ -13,12 +13,11 @@ import { TextInput, IThemedTextInput } from '../TextInput';
import { userTyping as userTypingAction } from '../../actions/room';
import styles from './styles';
import database from '../../lib/database';
import { emojis } from '../EmojiPicker/emojis';
import log, { events, logEvent } from '../../lib/methods/helpers/log';
import RecordAudio from './RecordAudio';
import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview';
import { themes } from '../../lib/constants';
import { themes, emojis } from '../../lib/constants';
import LeftButtons from './LeftButtons';
import RightButtons from './RightButtons';
import { canUploadFile } from '../../lib/methods/helpers/media';
@ -51,7 +50,8 @@ import {
TGetCustomEmoji,
TSubscriptionModel,
TThreadModel,
IMessage
IMessage,
IEmoji
} from '../../definitions';
import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
@ -59,6 +59,9 @@ 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';
import EmojiSearchbar from './EmojiSearchbar';
import shortnameToUnicode from '../../lib/methods/helpers/shortnameToUnicode';
require('./EmojiKeyboard');
@ -129,6 +132,7 @@ interface IMessageBoxState {
tshow: boolean;
mentionLoading: boolean;
permissionToUpload: boolean;
showEmojiSearchbar: boolean;
}
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -183,7 +187,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
command: {},
tshow: this.sendThreadToChannel,
mentionLoading: false,
permissionToUpload: true
permissionToUpload: true,
showEmojiSearchbar: false
};
this.text = '';
this.selection = { start: 0, end: 0 };
@ -209,6 +214,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
...videoPickerConfig,
...libPickerLabels
};
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
get sendThreadToChannel() {
@ -326,7 +333,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
tshow,
mentionLoading,
trackingType,
permissionToUpload
permissionToUpload,
showEmojiSearchbar
} = this.state;
const {
@ -346,6 +354,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true;
}
if (nextState.showEmojiSearchbar !== showEmojiSearchbar) {
return true;
}
if (!isFocused()) {
return false;
}
@ -438,6 +449,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
}
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
setOptions = async () => {
@ -519,7 +531,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}, 100);
onKeyboardResigned = () => {
this.closeEmoji();
const { showEmojiSearchbar } = this.state;
if (!showEmojiSearchbar) {
this.closeEmoji();
}
};
onPressMention = (item: any) => {
@ -577,18 +592,50 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}
};
onEmojiSelected = (keyboardId: string, params: { emoji: string }) => {
onKeyboardItemSelected = (keyboardId: string, params: { eventType: EventTypes; emoji: IEmoji }) => {
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);
let newCursor;
switch (eventType) {
case EventTypes.BACKSPACE_PRESSED:
logEvent(events.MB_BACKSPACE);
const emojiRegex = /\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]/;
let charsToRemove = 1;
const lastEmoji = text.substr(cursor > 0 ? cursor - 2 : text.length - 2, cursor > 0 ? cursor : text.length);
// Check if last character is an emoji
if (emojiRegex.test(lastEmoji)) charsToRemove = 2;
newText =
text.substr(0, (cursor > 0 ? cursor : text.length) - charsToRemove) + text.substr(cursor > 0 ? cursor : text.length);
newCursor = cursor - charsToRemove;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(newText !== '');
break;
case EventTypes.EMOJI_PRESSED:
logEvent(events.MB_EMOJI_SELECTED);
let emojiText = '';
if (typeof emoji === 'string') {
const shortname = `:${emoji}:`;
emojiText = shortnameToUnicode(shortname);
} else {
emojiText = `:${emoji.name}:`;
}
newText = `${text.substr(0, cursor)}${emojiText}${text.substr(cursor)}`;
newCursor = cursor + emojiText.length;
this.setInput(newText, { start: newCursor, end: newCursor });
this.setShowSend(true);
break;
case EventTypes.SEARCH_PRESSED:
logEvent(events.MB_EMOJI_SEARCH_PRESSED);
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: true });
break;
default:
// Do nothing
}
};
getPermalink = async (message: any) => {
@ -621,16 +668,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.setState({ mentions: res, mentionLoading: false });
}, 300);
getEmojis = debounce(async (keyword: any) => {
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
getCustomEmojis = async (keyword: any, count: number) => {
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, MENTIONS_COUNT_TO_DISPLAY);
const db = database.active;
const customEmojisCollection = db.get('custom_emojis');
const customEmojis = await (await customEmojisCollection.query(...whereClause).fetch()).slice(0, count);
return customEmojis;
};
getEmojis = debounce(async (keyword: any) => {
const customEmojis = await this.getCustomEmojis(keyword, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY);
const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
@ -881,7 +932,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
openEmoji = () => {
logEvent(events.ROOM_OPEN_EMOJI);
this.setState({ showEmojiKeyboard: true });
this.setState({ showEmojiKeyboard: true, showEmojiSearchbar: false });
this.stopTrackingMention();
};
recordingCallback = (recording: any) => {
@ -903,7 +955,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
};
closeEmoji = () => {
this.setState({ showEmojiKeyboard: false });
this.setState({ showEmojiKeyboard: false, showEmojiSearchbar: false });
};
closeEmojiKeyboardAndFocus = () => {
logEvent(events.ROOM_CLOSE_EMOJI);
this.closeEmoji();
this.focus();
};
closeEmojiAndAction = (action?: Function, params?: any) => {
@ -926,7 +984,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.clearInput();
this.debouncedOnChangeText.stop();
this.closeEmoji();
this.closeEmojiKeyboardAndFocus();
this.stopTrackingMention();
this.handleTyping(false);
if (message.trim() === '' && !showSend) {
@ -1083,10 +1141,34 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
);
};
renderEmojiSearchbar = () => {
const { showEmojiSearchbar } = this.state;
return showEmojiSearchbar ? (
<EmojiSearchbar
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
onEmojiSelected={(emoji: IEmoji) => {
this.onKeyboardItemSelected('EmojiKeyboard', { eventType: EventTypes.EMOJI_PRESSED, emoji });
}}
/>
) : null;
};
handleBackPress = () => {
const { showEmojiSearchbar } = this.state;
if (showEmojiSearchbar) {
this.setState({ showEmojiSearchbar: false });
return true;
}
return false;
};
renderContent = () => {
const {
recording,
showEmojiKeyboard,
showEmojiSearchbar,
showSend,
mentions,
trackingType,
@ -1149,11 +1231,11 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const textInputAndButtons = !recording ? (
<>
<LeftButtons
showEmojiKeyboard={showEmojiKeyboard}
showEmojiKeyboard={showEmojiKeyboard || showEmojiSearchbar}
editing={editing}
editCancel={this.editCancel}
openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji}
closeEmoji={this.closeEmojiKeyboardAndFocus}
/>
<TextInput
ref={component => (this.component = component)}
@ -1197,6 +1279,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
{recordAudio}
</View>
{this.renderSendToChannel()}
{this.renderEmojiSearchbar()}
</View>
{children}
</>
@ -1224,7 +1307,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
kbComponent={showEmojiKeyboard ? 'EmojiKeyboard' : null}
kbInitialProps={{ theme }}
onKeyboardResigned={this.onKeyboardResigned}
onItemSelected={this.onEmojiSelected}
onItemSelected={this.onKeyboardItemSelected}
trackInteractive
requiresSameParentToManageScrollView
addBottomView

View File

@ -105,7 +105,7 @@ export default StyleSheet.create({
},
emojiKeyboardContainer: {
flex: 1,
borderTopWidth: StyleSheet.hairlineWidth
borderTopWidth: 1
},
slash: {
height: 30,

View File

@ -23,7 +23,6 @@ interface IAllTabProps {
const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => {
const { colors } = useTheme();
const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name);
const server = useAppSelector(state => state.server.server);
const username = useAppSelector(state => state.login.user.username);
const count = item.usernames.length;
@ -50,7 +49,6 @@ const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemPro
content={item.emoji}
standardEmojiStyle={styles.allTabStandardEmojiStyle}
customEmojiStyle={styles.allTabCustomEmojiStyle}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
/>
<View style={styles.textContainer}>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { View } from 'react-native';
import { TGetCustomEmoji, IEmoji } from '../../definitions';
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions';
import ReactionsList from '.';
import { mockedStore as store } from '../../reducers/mockedStore';
import { updateSettings } from '../../actions/settings';
@ -11,7 +11,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' }
}[content] as IEmoji;
}[content] as ICustomEmoji;
return customEmoji;
};

View File

@ -8,7 +8,6 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import styles, { MIN_TAB_WIDTH } from './styles';
import { useDimensions, useOrientation } from '../../dimensions';
import { useAppSelector } from '../../lib/hooks';
interface ITabBarItem {
getCustomEmoji: TGetCustomEmoji;
@ -25,7 +24,6 @@ interface IReactionsTabBar {
const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
const { colors } = useTheme();
const server = useAppSelector(state => state.server.server);
return (
<Pressable
key={tab.emoji}
@ -46,7 +44,6 @@ const TabBarItem = ({ tab, index, goToPage, getCustomEmoji }: ITabBarItem) => {
content={tab.emoji}
standardEmojiStyle={styles.standardEmojiStyle}
customEmojiStyle={styles.customEmojiStyle}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: colors.auxiliaryTintColor }]}>{tab.usernames.length}</Text>

View File

@ -10,33 +10,24 @@ interface IEmoji {
literal: string;
isMessageContainsOnlyEmoji: boolean;
getCustomEmoji?: Function;
baseUrl: string;
customEmojis?: any;
style?: object;
onEmojiSelected?: Function;
tabEmojiStyle?: object;
}
const Emoji = React.memo(
({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, baseUrl, customEmojis = true, style = {} }: IEmoji) => {
const { colors } = useTheme();
const emojiUnicode = shortnameToUnicode(literal);
const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
if (emoji && customEmojis) {
return (
<CustomEmoji
baseUrl={baseUrl}
style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]}
emoji={emoji}
/>
);
}
return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
const Emoji = React.memo(({ literal, isMessageContainsOnlyEmoji, getCustomEmoji, customEmojis = true, style = {} }: IEmoji) => {
const { colors } = useTheme();
const emojiUnicode = shortnameToUnicode(literal);
const emoji: any = getCustomEmoji && getCustomEmoji(literal.replace(/:/g, ''));
if (emoji && customEmojis) {
return <CustomEmoji style={[isMessageContainsOnlyEmoji ? styles.customEmojiBig : styles.customEmoji, style]} emoji={emoji} />;
}
);
return (
<Text style={[{ color: colors.bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode}
</Text>
);
});
export default Emoji;

View File

@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
import Markdown, { MarkdownPreview } from '.';
import { themes } from '../../lib/constants';
import { TGetCustomEmoji, IEmoji } from '../../definitions/IEmoji';
import { TGetCustomEmoji, ICustomEmoji } from '../../definitions/IEmoji';
const theme = 'light';
@ -16,7 +16,6 @@ const styles = StyleSheet.create({
}
});
const baseUrl = 'https://open.rocket.chat';
const longText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const lineBreakText = `a
@ -34,7 +33,7 @@ const getCustomEmoji: TGetCustomEmoji = content => {
marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' }
}[content] as IEmoji;
}[content] as ICustomEmoji;
return customEmoji;
};
@ -108,13 +107,8 @@ export const Emoji = () => (
<View style={styles.container}>
<Markdown msg='Unicode: 😃😇👍' theme={theme} />
<Markdown msg='Shortnames: :joy::+1:' theme={theme} />
<Markdown
msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:'
theme={theme}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
/>
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
<Markdown msg='Custom emojis: :react_rocket: :nyan_rocket: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
<Markdown msg='😃 :+1: :marioparty:' theme={theme} getCustomEmoji={getCustomEmoji} />
</View>
);

View File

@ -33,7 +33,6 @@ interface IMarkdownProps {
md?: MarkdownAST;
mentions?: IUserMention[];
getCustomEmoji?: TGetCustomEmoji;
baseUrl?: string;
username?: string;
tmid?: string;
numberOfLines?: number;
@ -235,13 +234,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
};
renderEmoji = ({ literal }: TLiteral) => {
const { getCustomEmoji, baseUrl = '', customEmojis, style } = this.props;
const { getCustomEmoji, customEmojis, style } = this.props;
return (
<MarkdownEmoji
literal={literal}
isMessageContainsOnlyEmoji={this.isMessageContainsOnlyEmoji}
getCustomEmoji={getCustomEmoji}
baseUrl={baseUrl}
customEmojis={customEmojis}
style={style}
/>
@ -312,18 +310,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
};
render() {
const {
msg,
md,
mentions,
channels,
navToRoomInfo,
useRealName,
username = '',
getCustomEmoji,
baseUrl = '',
onLinkPress
} = this.props;
const { msg, md, mentions, channels, navToRoomInfo, useRealName, username = '', getCustomEmoji, onLinkPress } = this.props;
if (!msg) {
return null;
@ -333,7 +320,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return (
<NewMarkdown
username={username}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
useRealName={useRealName}
tokens={md}

View File

@ -3,7 +3,6 @@ import { Text } from 'react-native';
import { Emoji as EmojiProps } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../../lib/methods/helpers/shortnameToUnicode';
import { themes } from '../../../lib/constants';
import { useTheme } from '../../../theme';
import styles from '../styles';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
@ -15,21 +14,21 @@ interface IEmojiProps {
}
const Emoji = ({ block, isBigEmoji }: IEmojiProps) => {
const { theme } = useTheme();
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext);
const { colors } = useTheme();
const { getCustomEmoji } = useContext(MarkdownContext);
if ('unicode' in block) {
return <Text style={[{ color: themes[theme].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>;
return <Text style={[{ color: colors.bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{block.unicode}</Text>;
}
const emojiToken = block?.shortCode ? `:${block.shortCode}:` : `:${block.value?.value}:`;
const emojiUnicode = shortnameToUnicode(emojiToken);
const emoji = getCustomEmoji?.(block.value?.value);
if (emoji) {
return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
return <CustomEmoji style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
}
return (
<Text style={[{ color: themes[theme].bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}>
<Text style={[{ color: colors.bodyText }, isBigEmoji && emojiToken !== emojiUnicode ? styles.textBig : styles.text]}>
{emojiUnicode}
</Text>
);

View File

@ -7,7 +7,6 @@ interface IMarkdownContext {
channels?: IUserChannel[];
useRealName?: boolean;
username?: string;
baseUrl?: string;
navToRoomInfo?: Function;
getCustomEmoji?: Function;
onLinkPress?: Function;
@ -18,7 +17,6 @@ const defaultState = {
channels: [],
useRealName: false,
username: '',
baseUrl: '',
navToRoomInfo: () => {}
};

View File

@ -34,11 +34,8 @@ const getCustomEmoji = (content: string) => {
}[content];
return customEmoji;
};
const baseUrl = 'https://open.rocket.chat';
const NewMarkdown = ({ ...props }) => (
<NewMarkdownComponent baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />
);
const NewMarkdown = ({ ...props }) => <NewMarkdownComponent getCustomEmoji={getCustomEmoji} username='rocket.cat' {...props} />;
const simpleTextMsg = [
{
@ -340,7 +337,7 @@ const emojiTokens = [
export const Emoji = () => (
<View style={styles.container}>
<NewMarkdown tokens={bigEmojiTokens} />
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} />
</View>
);

View File

@ -24,7 +24,6 @@ interface IBodyProps {
navToRoomInfo?: Function;
useRealName?: boolean;
username: string;
baseUrl: string;
}
const Body = ({
@ -35,7 +34,6 @@ const Body = ({
username,
navToRoomInfo,
getCustomEmoji,
baseUrl,
onLinkPress
}: IBodyProps): React.ReactElement | null => {
if (isEmpty(tokens)) {
@ -51,7 +49,6 @@ const Body = ({
username,
navToRoomInfo,
getCustomEmoji,
baseUrl,
onLinkPress
}}
>

View File

@ -283,7 +283,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
<Markdown
msg={description}
style={[isReply && style]}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}

View File

@ -30,7 +30,7 @@ export const Item = () => (
user: { username: 'Marcos' }
}}
>
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => {}} timeFormat='LT' />
<CollapsibleQuote key={0} index={0} attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' />
</MessageContext.Provider>
</View>
);

View File

@ -82,7 +82,7 @@ interface IMessageReply {
const Fields = React.memo(
({ attachment, getCustomEmoji }: IMessageFields) => {
const { theme } = useTheme();
const { baseUrl, user } = useContext(MessageContext);
const { user } = useContext(MessageContext);
if (!attachment.fields) {
return null;
@ -97,7 +97,6 @@ const Fields = React.memo(
</Text>
<Markdown
msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}

View File

@ -16,7 +16,7 @@ import { MessageTypesValues } from '../../definitions';
const Content = React.memo(
(props: IMessageContent) => {
const { theme } = useTheme();
const { baseUrl, user, onLinkPress } = useContext(MessageContext);
const { user, onLinkPress } = useContext(MessageContext);
if (props.isInfo) {
// @ts-ignore
@ -54,7 +54,6 @@ const Content = React.memo(
<Markdown
msg={props.msg}
md={props.md}
baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username}

View File

@ -6,11 +6,11 @@ import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { IMessageEmoji } from './interfaces';
const Emoji = React.memo(
({ content, baseUrl, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => {
const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent);
if (emoji) {
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{shortnameToUnicode(content)}</Text>;
},

View File

@ -81,7 +81,6 @@ const ImageContainer = React.memo(
<Markdown
msg={file.description}
style={[isReply && style]}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}

View File

@ -47,7 +47,7 @@ const AddReaction = React.memo(({ theme }: { theme: TSupportedThemes }) => {
});
const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => {
const { onReactionPress, onReactionLongPress, baseUrl, user } = useContext(MessageContext);
const { onReactionPress, onReactionLongPress, user } = useContext(MessageContext);
const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1;
return (
<Touchable
@ -67,7 +67,6 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti
content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
/>
<Text style={[styles.reactionCount, { color: themes[theme].tintColor }]}>{reaction.usernames.length}</Text>

View File

@ -121,7 +121,7 @@ const Description = React.memo(
getCustomEmoji: TGetCustomEmoji;
theme: TSupportedThemes;
}) => {
const { baseUrl, user } = useContext(MessageContext);
const { user } = useContext(MessageContext);
const text = attachment.text || attachment.title;
if (!text) {
@ -132,7 +132,6 @@ const Description = React.memo(
<Markdown
msg={text}
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14 }]}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
@ -177,7 +176,7 @@ const Fields = React.memo(
theme: TSupportedThemes;
getCustomEmoji: TGetCustomEmoji;
}) => {
const { baseUrl, user } = useContext(MessageContext);
const { user } = useContext(MessageContext);
if (!attachment.fields) {
return null;
@ -188,13 +187,7 @@ const Fields = React.memo(
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
<Markdown
msg={field?.value || ''}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
<Markdown msg={field?.value || ''} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</View>
))}
</View>
@ -278,13 +271,7 @@ const Reply = React.memo(
) : null}
</View>
</Touchable>
<Markdown
msg={attachment.description}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
theme={theme}
/>
<Markdown msg={attachment.description} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</>
);
},

View File

@ -79,7 +79,6 @@ const Video = React.memo(
<>
<Markdown
msg={file.description}
baseUrl={baseUrl}
username={user.username}
getCustomEmoji={getCustomEmoji}
style={[isReply && style]}

View File

@ -67,7 +67,6 @@ export interface IMessageContent {
export interface IMessageEmoji {
content: string;
baseUrl: string;
standardEmojiStyle: { fontSize: number };
customEmojiStyle: StyleProp<ImageStyle>;
getCustomEmoji: TGetCustomEmoji;

View File

@ -1,43 +1,35 @@
import Model from '@nozbe/watermelondb/Model';
import { StyleProp } from 'react-native';
import { ImageStyle } from 'react-native-fast-image';
export interface IEmoji {
export interface IFrequentlyUsedEmoji {
content: string;
name: string;
extension: string;
extension?: string;
isCustom: boolean;
count?: number;
}
export interface ICustomEmojis {
[key: string]: Pick<IEmoji, 'name' | 'extension'>;
}
type TBasicEmoji = string;
export interface ICustomEmoji {
baseUrl?: string;
emoji: IEmoji;
style: StyleProp<ImageStyle>;
name: string;
extension: string;
}
export type IEmoji = ICustomEmoji | TBasicEmoji;
export interface ICustomEmojis {
[key: string]: ICustomEmoji;
}
export type TGetCustomEmoji = (name: string) => ICustomEmoji | null;
export type TFrequentlyUsedEmojiModel = IFrequentlyUsedEmoji & Model;
export interface ICustomEmojiModel {
_id: string;
name?: string;
name: string;
aliases?: string[];
extension: string;
_updatedAt: Date;
}
export interface IEmojiCategory {
baseUrl: string;
emojis: IEmoji[];
onEmojiSelected: (emoji: IEmoji) => void;
width: number | null;
style: StyleProp<ImageStyle>;
tabLabel: string;
}
export type TGetCustomEmoji = (name: string) => any;
export type TFrequentlyUsedEmojiModel = IEmoji & Model;
export type TCustomEmojiModel = ICustomEmojiModel & Model;

View File

@ -480,6 +480,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",

View File

@ -457,6 +457,7 @@
"Search_Messages": "Buscar Mensagens",
"Search": "Buscar",
"Search_by": "Buscar por",
"Search_emoji": "Buscar emoji",
"Search_global_users": "Busca por usuários globais",
"Search_global_users_description": "Caso ativado, busca por usuários de outras empresas ou servidores.",
"Security_and_privacy": "Segurança e privacidade",

View File

@ -70,6 +70,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#E4E7EA',
...mentions
},
dark: {
@ -122,6 +123,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#030b1b', // backgroundColor
...mentions
},
black: {
@ -174,6 +176,7 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
textInputSecondaryBackground: '#000000', // backgroundColor
...mentions
}
};

View File

@ -1,44 +1,44 @@
const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags'];
const tabs = [
{
tabLabel: '🕒',
tabLabel: 'clock',
category: list[0]
},
{
tabLabel: '🚀',
tabLabel: 'rocket',
category: list[1]
},
{
tabLabel: '😃',
tabLabel: 'emoji',
category: list[2]
},
{
tabLabel: '🐶',
tabLabel: 'leaf',
category: list[3]
},
{
tabLabel: '🍔',
tabLabel: 'burger',
category: list[4]
},
{
tabLabel: '',
tabLabel: 'basketball',
category: list[5]
},
{
tabLabel: '🚌',
tabLabel: 'airplane',
category: list[6]
},
{
tabLabel: '💡',
tabLabel: 'lamp-bulb',
category: list[7]
},
{
tabLabel: '💛',
tabLabel: 'percentage',
category: list[8]
},
{
tabLabel: '🏁',
tabLabel: 'flag',
category: list[9]
}
];
export default { list, tabs };
export const categories = { list, tabs };

View File

@ -2813,3 +2813,5 @@ export const emojis = [
'flag_tc',
'flag_mf'
];
export const DEFAULT_EMOJIS = ['clap', 'thumbsup', 'heart_eyes', 'grinning', 'thinking', 'smiley'];

View File

@ -0,0 +1,2 @@
export * from './emojis';
export * from './categories';

View File

@ -1,5 +1,6 @@
export * from './colors';
export * from './constantDisplayMode';
export * from './emojis';
export * from './environment';
export * from './keys';
export * from './links';

View File

@ -1,2 +1,3 @@
export * from './useAppSelector';
export * from './usePermissions';
export * from './useFrequentlyUsedEmoji';

View File

@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';
import { unstable_batchedUpdates } from 'react-native';
import { Q } from '@nozbe/watermelondb';
import database from '../database';
import { IEmoji } from '../../definitions';
import { DEFAULT_EMOJIS } from '../constants';
export const useFrequentlyUsedEmoji = (
withDefaultEmojis = false
): {
frequentlyUsed: IEmoji[];
loaded: boolean;
} => {
const [frequentlyUsed, setFrequentlyUsed] = useState<IEmoji[]>([]);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getFrequentlyUsedEmojis = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query(Q.experimentalSortBy('count', Q.desc)).fetch();
let frequentlyUsedEmojis = frequentlyUsedRecords.map(item => {
if (item.isCustom) {
return { name: item.content, extension: item.extension! }; // if isCustom is true, extension is not null
}
return item.content;
});
if (withDefaultEmojis && frequentlyUsedEmojis.length < DEFAULT_EMOJIS.length) {
frequentlyUsedEmojis = frequentlyUsedEmojis
.concat(DEFAULT_EMOJIS.filter(de => !frequentlyUsedEmojis.find(fue => typeof fue === 'string' && fue === de)))
.slice(0, DEFAULT_EMOJIS.length);
}
// TODO: remove once we update to React 18
unstable_batchedUpdates(() => {
setFrequentlyUsed(frequentlyUsedEmojis);
setLoaded(true);
});
};
getFrequentlyUsedEmojis();
}, []);
return { frequentlyUsed, loaded };
};

67
app/lib/methods/emojis.ts Normal file
View File

@ -0,0 +1,67 @@
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb';
import database from '../database';
import { IEmoji, TFrequentlyUsedEmojiModel } from '../../definitions';
import log from './helpers/log';
import { sanitizeLikeString } from '../database/utils';
import { emojis } from '../constants';
export const addFrequentlyUsed = async (emoji: IEmoji) => {
const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord: TFrequentlyUsedEmojiModel;
try {
if (typeof emoji === 'string') {
freqEmojiRecord = await freqEmojiCollection.find(emoji);
} else {
freqEmojiRecord = await freqEmojiCollection.find(emoji.name);
}
} catch (error) {
// Do nothing
}
try {
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update(f => {
if (f.count) {
f.count += 1;
}
});
} else {
await freqEmojiCollection.create(f => {
if (typeof emoji === 'string') {
f._raw = sanitizedRaw({ id: emoji }, freqEmojiCollection.schema);
Object.assign(f, { content: emoji, isCustom: false });
} else {
f._raw = sanitizedRaw({ id: emoji.name }, freqEmojiCollection.schema);
Object.assign(f, { content: emoji.name, extension: emoji.extension, isCustom: true });
}
f.count = 1;
});
}
});
} catch (e) {
log(e);
}
};
export const searchEmojis = async (keyword: string): Promise<IEmoji[]> => {
const likeString = sanitizeLikeString(keyword);
const whereClause = [];
if (likeString) {
whereClause.push(Q.where('name', Q.like(`${likeString}%`)));
}
const db = database.active;
const customEmojisCollection = await db
.get('custom_emojis')
.query(...whereClause)
.fetch();
const customEmojis = customEmojisCollection?.map(emoji => ({
name: emoji?.name,
extension: emoji?.extension
}));
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1);
return [...customEmojis, ...filteredEmojis];
};

View File

@ -187,6 +187,7 @@ export default {
ROOM_SEND_MESSAGE: 'room_send_message',
ROOM_ENCRYPTED_PRESS: 'room_encrypted_press',
ROOM_OPEN_EMOJI: 'room_open_emoji',
ROOM_CLOSE_EMOJI: 'room_close_emoji',
ROOM_AUDIO_RECORD: 'room_audio_record',
ROOM_AUDIO_RECORD_F: 'room_audio_record_f',
ROOM_AUDIO_FINISH: 'room_audio_finish',
@ -238,6 +239,13 @@ export default {
ROOM_MENTION_GO_USER_INFO: 'room_mention_go_user_info',
COMMAND_RUN: 'command_run',
COMMAND_RUN_F: 'command_run_f',
MB_BACKSPACE: 'mb_backspace',
MB_EMOJI_SELECTED: 'mb_emoji_selected',
MB_EMOJI_SEARCH_PRESSED: 'mb_emoji_search_pressed',
MB_SB_EMOJI_SEARCH: 'mb_sb_emoji_search',
MB_SB_EMOJI_SELECTED: 'mb_sb_emoji_selected',
REACTION_PICKER_EMOJI_SELECTED: 'reaction_picker_emoji_selected',
REACTION_PICKER_SEARCH_EMOJIS: 'reaction_picker_search_emojis',
// ROOM ACTIONS VIEW
RA_JITSI_VIDEO: 'ra_jitsi_video',

View File

@ -3,6 +3,7 @@ export * from './callJitsi';
export * from './canOpenRoom';
export * from './clearCache';
export * from './enterpriseModules';
export * from './emojis';
export * from './getCustomEmojis';
export * from './getPermalinks';
export * from './getPermissions';

View File

@ -23,13 +23,14 @@ import { IRoomInfoParam } from '../SearchMessagesView';
import {
IApplicationState,
TMessageModel,
IEmoji,
ISubscription,
SubscriptionType,
IAttachment,
IMessage,
TAnyMessageModel,
IUrl
IUrl,
TGetCustomEmoji,
ICustomEmoji
} from '../../definitions';
import { Services } from '../../lib/services';
@ -45,7 +46,7 @@ interface IMessagesViewProps {
StackNavigationProp<MasterDetailInsideStackParamList>
>;
route: RouteProp<ChatsStackParamList, 'MessagesView'>;
customEmojis: { [key: string]: IEmoji };
customEmojis: { [key: string]: ICustomEmoji };
theme: TSupportedThemes;
showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void;
useRealName: boolean;
@ -297,7 +298,7 @@ class MessagesView extends React.Component<IMessagesViewProps, IMessagesViewStat
}
};
getCustomEmoji = (name: string) => {
getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {

View File

@ -1,85 +1,52 @@
import React from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import Modal from 'react-native-modal';
import EmojiPicker from '../../containers/EmojiPicker';
import { isAndroid } from '../../lib/methods/helpers';
import { themes } from '../../lib/constants';
import { TSupportedThemes, withTheme } from '../../theme';
import styles from './styles';
import { IApplicationState } from '../../definitions';
const margin = isAndroid ? 40 : 20;
const maxSize = 400;
import { IEmoji } from '../../definitions';
import { EventTypes } from '../../containers/EmojiPicker/interfaces';
import { searchEmojis } from '../../lib/methods';
import { useDebounce } from '../../lib/methods/helpers/debounce';
import { EmojiSearch } from '../../containers/EmojiPicker/EmojiSearch';
import { events, logEvent } from '../../lib/methods/helpers/log';
interface IReactionPickerProps {
message?: any;
show: boolean;
isMasterDetail: boolean;
reactionClose: () => void;
onEmojiSelected: (shortname: string, id: string) => void;
width: number;
height: number;
theme: TSupportedThemes;
onEmojiSelected: (emoji: IEmoji, id: string) => void;
}
class ReactionPicker extends React.Component<IReactionPickerProps> {
shouldComponentUpdate(nextProps: IReactionPickerProps) {
const { show, width, height } = this.props;
return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
}
const ReactionPicker = ({ onEmojiSelected, message, reactionClose }: IReactionPickerProps): React.ReactElement => {
const [searchedEmojis, setSearchedEmojis] = React.useState<IEmoji[]>([]);
const [searching, setSearching] = React.useState<boolean>(false);
onEmojiSelected = (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) {
onEmojiSelected(shortname || emoji, message.id);
}
const handleTextChange = useDebounce((text: string) => {
setSearching(text !== '');
handleSearchEmojis(text);
}, 300);
const handleSearchEmojis = async (text: string) => {
logEvent(events.REACTION_PICKER_SEARCH_EMOJIS);
const emojis = await searchEmojis(text);
setSearchedEmojis(emojis);
};
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;
const handleEmojiSelect = (_eventType: EventTypes, emoji?: IEmoji) => {
logEvent(events.REACTION_PICKER_EMOJI_SELECTED);
if (message && emoji) {
onEmojiSelected(emoji, message.id);
}
reactionClose();
};
return show ? (
<Modal
isVisible={show}
style={{ alignItems: 'center' }}
onBackdropPress={reactionClose}
onBackButtonPress={reactionClose}
animationIn='fadeIn'
animationOut='fadeOut'
backdropOpacity={themes[theme].backdropOpacity}
>
<View
style={[
styles.reactionPickerContainer,
{
width: widthStyle,
height: heightStyle
}
]}
testID='reaction-picker'
>
<EmojiPicker theme={theme} onEmojiSelected={this.onEmojiSelected} />
</View>
</Modal>
) : null;
}
}
return (
<View style={styles.reactionPickerContainer} testID='reaction-picker'>
<View style={styles.reactionSearchContainer}>
<EmojiSearch onChangeText={handleTextChange} bottomSheet />
</View>
<EmojiPicker onItemClicked={handleEmojiSelect} searching={searching} searchedEmojis={searchedEmojis} />
</View>
);
};
const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(withTheme(ReactionPicker));
export default ReactionPicker;

View File

@ -74,8 +74,9 @@ import {
TMessageModel,
TSubscriptionModel,
TThreadModel,
ICustomEmojis,
IEmoji,
ICustomEmojis
TGetCustomEmoji
} from '../../definitions';
import { E2E_MESSAGE_TYPE, E2E_STATUS, MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad, themes } from '../../lib/constants';
import { TListRef } from './List/List';
@ -112,7 +113,6 @@ const stateAttrsUpdate = [
'loading',
'editing',
'replying',
'reacting',
'readOnly',
'member',
'canForwardGuest',
@ -164,7 +164,6 @@ interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackPar
isMasterDetail: boolean;
replyBroadcast: Function;
width: number;
height: number;
insets: EdgeInsets;
transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type
viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type
@ -200,7 +199,6 @@ interface IRoomViewState {
editing: boolean;
replying: boolean;
replyWithMention: boolean;
reacting: boolean;
readOnly: boolean;
unreadsCount: number | null;
roomUserId?: string | null;
@ -274,7 +272,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
editing: false,
replying: !!selectedMessage,
replyWithMention: false,
reacting: false,
readOnly: false,
unreadsCount: null,
roomUserId,
@ -828,12 +825,27 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false });
};
showReactionPicker = () => {
const { showActionSheet } = this.props;
const { selectedMessage } = this.state;
showActionSheet({
children: (
<ReactionPicker message={selectedMessage} onEmojiSelected={this.onReactionPress} reactionClose={this.onReactionClose} />
),
snaps: [400],
enableContentPanningGesture: false
});
};
onReactionInit = (message: TAnyMessageModel) => {
this.setState({ selectedMessage: message, reacting: true });
this.messagebox?.current?.closeEmojiAndAction(() => {
this.setState({ selectedMessage: message }, this.showReactionPicker);
});
};
onReactionClose = () => {
this.setState({ selectedMessage: undefined, reacting: false });
const { hideActionSheet } = this.props;
this.setState({ selectedMessage: undefined }, hideActionSheet);
};
onMessageLongPress = (message: TAnyMessageModel) => {
@ -850,8 +862,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
navigation.navigate('AttachmentView', { attachment });
};
onReactionPress = async (shortname: string, messageId: string) => {
onReactionPress = async (emoji: IEmoji, messageId: string) => {
try {
let shortname = '';
if (typeof emoji === 'string') {
shortname = emoji;
} else {
shortname = emoji.name;
}
await Services.setReaction(shortname, messageId);
this.onReactionClose();
Review.pushPositiveEvent();
@ -1026,7 +1044,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
});
};
getCustomEmoji = (name: string): Pick<IEmoji, 'name' | 'extension'> | null => {
getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {
@ -1489,8 +1507,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
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;
@ -1521,15 +1539,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
/>
{this.renderFooter()}
{this.renderActions()}
<ReactionPicker
show={reacting}
message={selectedMessage}
onEmojiSelected={this.onReactionPress}
reactionClose={this.onReactionClose}
width={width}
height={height}
theme={theme}
/>
<UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} />
<JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} />
</SafeAreaView>

View File

@ -14,10 +14,13 @@ export default StyleSheet.create({
alignItems: 'center',
marginVertical: 15
},
reactionSearchContainer: {
marginHorizontal: 12,
marginBottom: 8
},
reactionPickerContainer: {
borderRadius: 4,
flexDirection: 'column',
overflow: 'hidden'
flex: 1,
flexDirection: 'column'
},
bannerContainer: {
paddingVertical: 12,
@ -64,5 +67,14 @@ export default StyleSheet.create({
previewMode: {
fontSize: 16,
...sharedStyles.textMedium
},
searchbarContainer: {
height: 56,
marginBottom: 8,
paddingHorizontal: 12
},
reactionPickerSearchbar: {
paddingHorizontal: 20,
minHeight: 48
}
});

View File

@ -31,11 +31,12 @@ import {
IUser,
TMessageModel,
IUrl,
IEmoji,
IAttachment,
ISubscription,
SubscriptionType,
TSubscriptionModel
TSubscriptionModel,
TGetCustomEmoji,
ICustomEmoji
} from '../../definitions';
import { Services } from '../../lib/services';
@ -68,7 +69,7 @@ interface ISearchMessagesViewProps extends INavigationOption {
baseUrl: string;
serverVersion: string;
customEmojis: {
[key: string]: IEmoji;
[key: string]: ICustomEmoji;
};
theme: TSupportedThemes;
useRealName: boolean;
@ -201,7 +202,7 @@ class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISear
await this.getMessages(searchText, true);
}, 1000);
getCustomEmoji = (name: string) => {
getCustomEmoji: TGetCustomEmoji = name => {
const { customEmojis } = this.props;
const emoji = customEmojis[name];
if (emoji) {

View File

@ -107,7 +107,9 @@ describe('Profile screen', () => {
it('should change email and password', async () => {
await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${data.random}@rocket.chat`);
await element(by.id('profile-view-new-password')).replaceText(`${profileChangeUser.password}new`);
await sleep(300);
await waitFor(element(by.id('profile-view-submit')))
.toExist()
.withTimeout(2000);
await element(by.id('profile-view-submit')).tap();
await waitFor(element(by.id('profile-view-enter-password-sheet')))
.toBeVisible()

View File

@ -8,8 +8,6 @@ import {
tapBack,
sleep,
searchRoom,
starMessage,
pinMessage,
dismissReviewNag,
tryTapping,
platformTypes,
@ -63,9 +61,7 @@ describe('Room screen', () => {
});
it('should have open emoji button', async () => {
if (device.getPlatform() === 'android') {
await expect(element(by.id('messagebox-open-emoji'))).toExist();
}
await expect(element(by.id('messagebox-open-emoji'))).toExist();
});
it('should have message input', async () => {
@ -89,24 +85,110 @@ describe('Room screen', () => {
await expect(element(by[textMatcher](`${data.random}message`)).atIndex(0)).toExist();
});
it('should show/hide emoji keyboard', async () => {
if (device.getPlatform() === 'android') {
describe('Emoji Keyboard', () => {
it('should open emoji keyboard, select an emoji and send it', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toExist()
.withTimeout(10000);
await expect(element(by.id('messagebox-close-emoji'))).toExist();
await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible();
await expect(element(by.id('emoji-picker-tab-emoji'))).toExist();
await element(by.id('emoji-picker-tab-emoji')).tap();
await expect(element(by.id('emoji-blush'))).toExist();
await element(by.id('emoji-blush')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('😊');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by[textMatcher]('😊')))
.toExist()
.withTimeout(60000);
await element(by[textMatcher]('😊')).atIndex(0).tap();
});
it('should open emoji keyboard, select an emoji and delete it using emoji keyboards backspace', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toExist()
.withTimeout(10000);
await expect(element(by.id('emoji-picker-tab-emoji'))).toExist();
await element(by.id('emoji-picker-tab-emoji')).tap();
await expect(element(by.id('emoji-upside_down'))).toExist();
await element(by.id('emoji-upside_down')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('🙃');
await waitFor(element(by.id('emoji-picker-backspace')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-picker-backspace')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('');
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.toBeNotVisible()
.not.toBeVisible()
.withTimeout(10000);
await expect(element(by.id('messagebox-close-emoji'))).toBeNotVisible();
await expect(element(by.id('messagebox-open-emoji'))).toExist();
}
});
it('should search emoji and send it', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-search')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-picker-search')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-searchbar-input')).replaceText('no_mouth');
await waitFor(element(by.id('emoji-no_mouth')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-no_mouth')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('😶');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by[textMatcher]('😶')))
.toExist()
.withTimeout(60000);
await element(by[textMatcher]('😶')).atIndex(0).tap();
});
it('should search emojis, go back to Emoji keyboard and then close the Emoji keyboard', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-search')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-picker-search')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('openback-emoji-keyboard')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.not.toBeVisible()
.withTimeout(2000);
await expect(element(by.id('messagebox-close-emoji'))).toExist();
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.not.toBeVisible()
.withTimeout(10000);
});
it('frequently used emojis should contain the recently used emojis', async () => {
await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('emoji-picker-tab-clock')));
await element(by.id('emoji-picker-tab-clock')).tap();
await waitFor(element(by.id('emoji-blush')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('emoji-upside_down')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('emoji-no_mouth')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('messagebox-close-emoji'))).toExist();
await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji')))
.not.toBeVisible()
.withTimeout(10000);
});
});
it('should show/hide emoji autocomplete', async () => {
await element(by.id('messagebox-input')).clearText();
await element(by.id('messagebox-input')).typeText(':joy');
await sleep(300);
await waitFor(element(by.id('messagebox-container')))
@ -223,10 +305,8 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by[textMatcher]('Permalink')).atIndex(0).tap();
// TODO: test clipboard
});
it('should copy message', async () => {
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
@ -237,28 +317,9 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by[textMatcher]('Copy')).atIndex(0).tap();
// TODO: test clipboard
});
it('should star message', async () => {
await starMessage('message');
await sleep(1000); // https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2324
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'slow', 0.5);
await waitFor(element(by[textMatcher]('Unstar')).atIndex(0))
.toExist()
.withTimeout(6000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
});
it('should react to message', async () => {
await waitFor(element(by[textMatcher](`${data.random}message`)))
.toExist()
@ -275,19 +336,47 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.id('add-reaction')).tap();
await waitFor(element(by.id('reaction-picker')))
.toBeVisible()
await waitFor(element(by.id('emoji-picker-tab-emoji')))
.toExist()
.withTimeout(2000);
await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grinning')))
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 1);
await element(by.id('emoji-picker-tab-emoji')).tap();
await waitFor(element(by.id('emoji-grinning')))
.toExist()
.withTimeout(10000);
await element(by.id('reaction-picker-grinning')).tap();
await element(by.id('emoji-grinning')).tap();
await waitFor(element(by.id('message-reaction-:grinning:')))
.toExist()
.withTimeout(60000);
});
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should search emojis in the reaction picker and react', async () => {
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.id('add-reaction')).tap();
await waitFor(element(by.id('emoji-searchbar-input')))
.toExist()
.withTimeout(2000);
await element(by.id('emoji-searchbar-input')).typeText('laughing');
await waitFor(element(by.id('emoji-laughing')))
.toExist()
.withTimeout(4000);
await element(by.id('emoji-laughing')).tap();
await waitFor(element(by.id('message-reaction-:laughing:')))
.toExist()
.withTimeout(60000);
});
it('should react to message with frequently used emoji', async () => {
await element(by[textMatcher](`${data.random}message`))
.atIndex(0)
@ -297,37 +386,38 @@ describe('Room screen', () => {
.withTimeout(2000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.id('message-actions-emoji-+1')))
await waitFor(element(by.id('message-actions-emoji-upside_down')))
.toBeVisible()
.withTimeout(2000);
await element(by.id('message-actions-emoji-+1')).tap();
await waitFor(element(by.id('message-reaction-:+1:')))
await element(by.id('message-actions-emoji-upside_down')).tap();
await waitFor(element(by.id('message-reaction-:upside_down:')))
.toBeVisible()
.withTimeout(60000);
});
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async () => {
await element(by.id('message-add-reaction')).tap();
await waitFor(element(by.id('reaction-picker')))
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(2000);
await waitFor(element(by.id('reaction-picker-grinning')))
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 1);
await waitFor(element(by.id('emoji-upside_down')))
.toExist()
.withTimeout(4000);
await waitFor(element(by.id('emoji-picker-tab-emoji')))
.toExist()
.withTimeout(2000);
await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grimacing')))
await element(by.id('emoji-picker-tab-emoji')).tap();
await waitFor(element(by.id('emoji-wink')))
.toExist()
.withTimeout(2000);
await element(by.id('reaction-picker-grimacing')).tap();
await waitFor(element(by.id('message-reaction-:grimacing:')))
.withTimeout(10000);
await element(by.id('emoji-wink')).tap();
await waitFor(element(by.id('message-reaction-:wink:')))
.toExist()
.withTimeout(60000);
});
it('should ask for review', async () => {
await dismissReviewNag(); // TODO: Create a proper test for this elsewhere.
});
it('should open/close reactions list', async () => {
await element(by.id('message-reaction-:grinning:')).longPress();
await waitFor(element(by.id('reactionsList')))
@ -340,7 +430,7 @@ describe('Room screen', () => {
it('should remove reaction', async () => {
await element(by.id('message-reaction-:grinning:')).tap();
await waitFor(element(by.id('message-reaction-:grinning:')))
.toBeNotVisible()
.not.toExist()
.withTimeout(60000);
});
@ -364,7 +454,6 @@ describe('Room screen', () => {
.toExist()
.withTimeout(60000);
});
it('should quote message', async () => {
await mockMessage('quote');
await element(by[textMatcher](`${data.random}quote`))
@ -381,34 +470,9 @@ describe('Room screen', () => {
.toExist()
.withTimeout(2000);
await element(by.id('messagebox-send-message')).tap();
// TODO: test if quote was sent
});
it('should pin message', async () => {
await mockMessage('pin');
await pinMessage('pin');
await waitFor(element(by[textMatcher](`${data.random}pin`)).atIndex(0))
.toExist()
.withTimeout(5000);
await waitFor(element(by[textMatcher](`${data.users.regular.username} Message pinned`)).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by[textMatcher](`${data.random}pin`))
.atIndex(0)
.longPress();
await waitFor(element(by.id('action-sheet')))
.toExist()
.withTimeout(1000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by[textMatcher]('Unpin')).atIndex(0))
.toExist()
.withTimeout(2000);
await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8);
});
it('should delete message', async () => {
await mockMessage('delete');
await waitFor(element(by[textMatcher](`${data.random}delete`)).atIndex(0)).toBeVisible();
@ -424,7 +488,6 @@ describe('Room screen', () => {
.toExist()
.withTimeout(1000);
await element(by[textMatcher]('Delete')).atIndex(0).tap();
const deleteAlertMessage = 'You will not be able to recover this message!';
await waitFor(element(by[textMatcher](deleteAlertMessage)).atIndex(0))
.toExist()

View File

@ -192,8 +192,8 @@ describe('Room info screen', () => {
await element(by.id('room-info-edit-view-announcement')).replaceText('abc');
await element(by.id('room-info-edit-view-password')).replaceText('abc');
await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5);
// await element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032
await swipe('up');
await element(by.id('room-info-edit-view-ro')).longPress(); // https://github.com/facebook/react-native/issues/28032
await element(by.id('room-info-edit-view-react-when-ro')).tap();
await swipe('up');
await element(by.id('room-info-edit-view-reset')).tap();
@ -206,8 +206,8 @@ describe('Room info screen', () => {
await expect(element(by.id('room-info-edit-view-password'))).toHaveText('');
// await swipe('down');
await expect(element(by.id('room-info-edit-view-t'))).toHaveToggleValue(true);
await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(true);
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toHaveToggleValue(false);
await expect(element(by.id('room-info-edit-view-ro'))).toHaveToggleValue(false);
await expect(element(by.id('room-info-edit-view-react-when-ro'))).toBeNotVisible();
await swipe('down');
});